Algebraic Data Types: Product ve Sum Type, Discriminated Union ve "Illegal State'i Temsil Edilemez Yap"
Algebraic data type, tipleri "ve" ile (product type) ya da "veya" ile (sum type) birleştirerek modeldeki kombinasyon sayısını iş kuralındaki durum sayısına eşitler; böylece imkânsız durum oluşturulamaz. Bookstore'un bayraklı sipariş durumunu abstract record ile sum type'a çeviriyor, geçişleri tiplerle yazıyor, C# 14'ün neden _ kolu istediğini, TypeScript discriminated union'ı, JSON polymorphism'i ve C# 15 union type'larını gerçek derleyici çıktılarıyla gösteriyoruz.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 (SDK 10.0.402) ile, TypeScript örnekleri TypeScript 7.0.2 ve Node.js 26 ile derlenip denenmiştir. "C# 15 ile: union Type'ları" bölümündeki kod ayrı bir projede C# 15 ve .NET 11 SDK (11.0.100) ile derlenip çalıştırılmıştır.
Önce Tahmin Et: İptal Edilen Sipariş Kargoya Verilirse
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) siparişin durumu yıllardır üç bayrak ve üç nullable property ile tutuluyor:
public class FlaggedOrder
{
public bool IsPaid { get; set; }
public bool IsShipped { get; set; }
public bool IsCancelled { get; set; }
public string? PaymentId { get; set; }
public string? TrackingNo { get; set; }
public string? CancelReason { get; set; }
public string Describe()
{
if (IsCancelled)
return $"cancelled: {CancelReason}";
if (IsShipped)
return $"shipped with {TrackingNo}";
if (IsPaid)
return $"paid with {PaymentId}";
return "pending";
}
}
Bir cuma öğleden sonra müşteri hizmetleri ödenmiş bir siparişi müşterinin isteğiyle iptal ediyor; aynı dakikada depo aynı siparişi kargoya veriyor. Bir de testte elle oluşturulmuş, yalnız IsShipped bayrağı açık bir sipariş var. Ekrana ne yazılır?
FlaggedOrder order = new() { IsPaid = true, PaymentId = "PAY-1001" };
// Müşteri hizmetleri iptal ediyor; depo aynı dakikada kargoya veriyor
order.IsCancelled = true;
order.CancelReason = "customer request";
order.IsShipped = true;
order.TrackingNo = "TRK-778";
Console.WriteLine(order.Describe());
Console.WriteLine($"paid: {order.IsPaid}, shipped: {order.IsShipped}, cancelled: {order.IsCancelled}");
FlaggedOrder ghost = new() { IsShipped = true };
Console.WriteLine($"ghost: {ghost.Describe()}");
Cevap: Derleyicinin Kabul Ettiği İmkânsız Durumlar
$ dotnet run --project src/Bookstore.Fp.Demo -- 05-flags
cancelled: customer request
paid: True, shipped: True, cancelled: True
ghost: shipped with
Sipariş hem iptal edilmiş hem kargoya verilmiş. Describe if'lerin sırasına göre "cancelled" diyor; müşteri iade beklerken kitap yolda. İkinci sipariş ödenmeden kargoya verilmiş ve takip numarası yok; Describe boş bir takip numarası basıyor. İki durum da iş açısından imkânsız, ama derleyici için tamamen geçerli: üç bool sekiz kombinasyon üretir ve tip sistemi hangisinin anlamlı olduğunu bilmez.

Sekiz kombinasyonun üçü imkânsız, ikisi aynı durumu (iptal edilmiş sipariş) iki ayrı biçimde yazıyor. Üç nullable string de eklenince tablo büyüyor: IsPaid açık ama PaymentId null, IsCancelled kapalı ama CancelReason dolu... Bu imkânsız durumlara karşı bugüne kadar yaptığımız şey, her method'un başına doğrulama yazmak ve hepsini hatırlamayı ummaktı. Functional programming'in cevabı başka: imkânsız durumu oluşturulamaz hâle getirmek. Yaron Minsky'nin OCaml üzerine verdiği konuşmalarla yaygınlaşan ifadeyle "make illegal states unrepresentable". Bunun aracı algebraic data type (ADT; diğer tiplerin "ve" ile birleştirilmesi ya da "veya" ile seçenek olarak sunulmasıyla kurulan tip).
Product Type ve Sum Type: Neden "Algebraic"?
Tipleri birleştirmenin iki temel yolu var. Product type birden fazla değeri "ve" ile bir araya getirir: bir Order'ın sipariş numarası ve müşterisi ve tutarı ve durumu var. C#'taki record, class, struct ve tuple'lar, TypeScript'teki object tipleri product type'tır. Adı, alabileceği değer sayısından gelir: üç bool property'si olan bir record 2 × 2 × 2 = 8 farklı değer alabilir. Değer sayıları çarpılır.
Sum type ise seçenekleri "veya" ile birleştirir: sipariş ya bekliyor ya ödenmiş ya kargoda ya da iptal edilmiş; aynı anda yalnızca biri. Her seçeneğin kendi verisi olabilir: ödenmiş siparişin ödeme numarası, kargodakinin takip numarası, iptal edilenin sebebi var. Değer sayıları toplanır: dört seçenek, dört durum (her seçeneğin kendi verisinin değer sayısıyla birlikte). F#, Haskell, OCaml, Rust ve Swift'te sum type dilin temel parçası; TypeScript'te discriminated union olarak var. C#'ta ise C# 15'e kadar doğrudan bir karşılığı yoktu; elimizdeki araçlarla taklit ediyoruz.
"Algebraic" adı bu çarpma ve toplamadan geliyor. Pratikteki anlamı basit: modeldeki kombinasyon sayısını iş kuralındaki durum sayısına eşitlemek. Bayraklı class 8 kombinasyon ve 3 nullable property ile iş kuralındaki 4 durumu temsil etmeye çalışıyor; sum type tam olarak 4 durum sunuyor.
C# 14'te Sum Type: abstract record ve sealed Case'ler
C# 14'te sum type'a en yakın yapı, abstract bir record ve onun içinde tanımlanmış sealed alt record'lar:
[JsonPolymorphic(TypeDiscriminatorPropertyName = "kind")]
[JsonDerivedType(typeof(Pending), "pending")]
[JsonDerivedType(typeof(Paid), "paid")]
[JsonDerivedType(typeof(Shipped), "shipped")]
[JsonDerivedType(typeof(Cancelled), "cancelled")]
public abstract record OrderStatus
{
// Yalnızca aşağıdaki iç içe case'ler OrderStatus'tan türeyebilir
private OrderStatus() { }
public sealed record Pending : OrderStatus;
public sealed record Paid(string PaymentId) : OrderStatus;
public sealed record Shipped(string PaymentId, string TrackingNo) : OrderStatus;
public sealed record Cancelled(string Reason) : OrderStatus;
}
Her case yalnızca kendi verisini taşıyor: Pending'in hiçbir property'si yok, Shipped hem ödeme hem takip numarasını zorunlu tutuyor. "Kargoda ama takip numarası yok" artık yazılamıyor; Shipped'in constructor'ı iki string'i de istiyor. Private constructor dışarıdan yeni bir case türetilmesini engellemek için; iç içe tanımlanan case'ler base'in private member'larına erişebildiği için onlar türeyebiliyor. JSON attribute'larına Tuzaklar bölümünde döneceğiz.
Order ise bir product type; durumu tek bir property:
public sealed record Order(string OrderNo, string CustomerEmail, decimal Total, OrderStatus Status);
Durumu okuyan kod switch expression ile her case'i ayrı ele alıyor. Property ve type pattern'lerin syntax'ı C# İleri serisinin "C#'ta Pattern Matching Derinlemesine: Property, Positional ve Tuple Pattern'ler" yazısında:
public static string Describe(OrderStatus status) => status switch
{
Pending => "pending",
Paid paid => $"paid with {paid.PaymentId}",
Shipped shipped => $"shipped with {shipped.TrackingNo}",
Cancelled cancelled => $"cancelled: {cancelled.Reason}",
_ => throw new UnreachableException($"Unknown status {status.GetType().Name}")
};
paid.PaymentId null kontrolü gerektirmiyor: Paid case'ine girdiysen ödeme numarası kesin var. Bayraklı modeldeki if (IsPaid && PaymentId != null) kombinasyonları ortadan kalkıyor; tip, kontrolün kendisi oluyor.
Geçişleri Tiplerle Yazmak: Ship Yalnızca Paid Alır
Sum type'ın asıl kazancı durumlar arasındaki geçişlerde ortaya çıkıyor. Geçişleri, yalnızca geçerli başlangıç durumunu kabul eden pure fonksiyonlar olarak yazalım:
public static class OrderWorkflow
{
public static Paid Pay(Pending pending, string paymentId) => new(paymentId);
public static Shipped Ship(Paid paid, string trackingNo) => new(paid.PaymentId, trackingNo);
// Kargodaki sipariş iptal edilemez: yalnızca bu iki overload var
public static Cancelled Cancel(Pending pending, string reason) => new(reason);
public static Cancelled Cancel(Paid paid, string reason) => new(reason);
}
Pending pending = new();
Paid paid = OrderWorkflow.Pay(pending, "PAY-1001");
Shipped shipped = OrderWorkflow.Ship(paid, "TRK-778");
Cancelled cancelled = OrderWorkflow.Cancel(paid, "customer request");
OrderStatus[] history = [pending, paid, shipped, cancelled];
foreach (OrderStatus status in history)
Console.WriteLine(OrderWorkflow.Describe(status));
Order order = new("BS-2026-0042", "ada@example.com", 259.36m, shipped);
Console.WriteLine(order);
$ dotnet run --project src/Bookstore.Fp.Demo -- 05-workflow
pending
paid with PAY-1001
shipped with TRK-778
cancelled: customer request
Order { OrderNo = BS-2026-0042, CustomerEmail = ada@example.com, Total = 259.36, Status = Shipped { PaymentId = PAY-1001, TrackingNo = TRK-778 } }
Ship'in parametresi OrderStatus değil Paid. Ödenmemiş bir siparişi kargoya vermeye ya da kargodaki siparişi iptal etmeye çalışan kod derlenmiyor:
Pending pending = new();
Shipped shipped = OrderWorkflow.Ship(pending, "TRK-778");
Cancelled late = OrderWorkflow.Cancel(shipped, "too late");
Program.cs(5,38): error CS1503: Argument 1: cannot convert from 'Bookstore.Ordering.Orders.OrderStatus.Pending' to 'Bookstore.Ordering.Orders.OrderStatus.Paid'
Program.cs(6,39): error CS1503: Argument 1: cannot convert from 'Bookstore.Ordering.Orders.OrderStatus.Shipped' to 'Bookstore.Ordering.Orders.OrderStatus.Pending'
Tahmin sorusundaki "iptal edilmiş ama kargoya verilmiş" sipariş bu modelle bir runtime hatası değil, bir derleme hatası. Aynı anda iki işlemin yarışması sorunu (müşteri hizmetleri ile deponun aynı dakikada işlem yapması) tabii ki tiple çözülmez; o, veritabanında concurrency kontrolünün işi. Ama hangi geçişin geçerli olduğu artık kodun her yerine dağılmış if'lerde değil, fonksiyon imzalarında.
Cancel'ın iki overload'ına dikkat. "Pending ya da Paid" diye bir tip C# 14'te yok; ya iki overload yazıyorsun ya da OrderStatus alıp içeride kontrol ediyorsun (ve kontrolü runtime'a bırakıyorsun). Bu eksikliği C# 15 kapatıyor; birazdan göreceğiz.
Exhaustiveness: Derleyici Neden Hâlâ _ İstiyor?
Describe'daki son kola, _ => throw new UnreachableException(...) satırına dikkat. Dört case'in dördü de ele alınmışken neden gerekli? Satırı silelim:
static string Short(OrderStatus status) => status switch
{
Pending => "P",
Paid => "$",
Shipped => "S",
Cancelled => "X"
};
Program.cs(8,51): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '_' is not covered.
Derleyici dört case'i görüyor ama OrderStatus'ın başka bir alt tipi olmadığından emin olamıyor. C# 14'te bir class hiyerarşisini "kapalı" ilan etmenin yolu yok. Private constructor hilesi de tam kapatmıyor: record'ların derleyicinin ürettiği bir copy constructor'ı var ve sealed olmayan bir record'da bu constructor'ın public ya da protected olması zorunlu. Private yapmaya çalışınca derleyici izin vermiyor:
c.cs(4,13): error CS8878: A copy constructor 'OrderStatus.OrderStatus(OrderStatus)' must be public or protected because the record is not sealed.
Protected copy constructor üzerinden başka bir assembly'de yeni bir case türetmek mümkün ve bu kod derleniyor:
OrderStatus refunded = new Refunded("damaged copy");
Console.WriteLine(OrderWorkflow.Describe(refunded));
record Refunded(string Reason) : OrderStatus(new Pending());
Unhandled exception. System.Diagnostics.UnreachableException: Unknown status Refunded
Derleyicinin _ kolunu istemesi bu yüzden haklı. O kola bir şey koymak zorundasın; sessizce varsayılan bir değer döndürmek (_ => "unknown") yerine UnreachableException fırlatmak, beklenmeyen bir case'in fark edilmeden geçmesini engelliyor. Bedeli şu: OrderStatus'a beşinci bir case eklediğinde derleyici bu switch'te eksik kolu sana söylemez, çünkü _ her şeyi yakalıyor. Hatayı runtime'da, testlerde ya da üretimde görürsün. Gerçek bir sum type'ın vaadi tam olarak bu uyarıyı derleme zamanında almak.
TypeScript'te Discriminated Union ve never
TypeScript'te sum type dilin parçası. Her case ortak bir literal property (burada kind) taşıyor; derleyici bu property'ye bakarak tipi daraltıyor (narrowing):
export type OrderStatus =
| { readonly kind: "pending" }
| { readonly kind: "paid"; readonly paymentId: string }
| { readonly kind: "shipped"; readonly paymentId: string; readonly trackingNo: string }
| { readonly kind: "cancelled"; readonly reason: string };
export function assertNever(value: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}
export function describe(status: OrderStatus): string {
switch (status.kind) {
case "pending":
return "pending";
case "paid":
return `paid with ${status.paymentId}`;
case "shipped":
return `shipped with ${status.trackingNo}`;
case "cancelled":
return `cancelled: ${status.reason}`;
default:
return assertNever(status);
}
}
default koluna gelindiğinde bütün case'ler elenmiş olduğu için status'ın tipi never. Union'a yeni bir case eklenip switch'e eklenmezse never artık doğru değil ve derleyici hata veriyor. Aynı tip sistemi, bir case'e ait olmayan property'yi de reddediyor:
type OrderStatusV2 =
| { kind: "pending" }
| { kind: "paid"; paymentId: string }
| { kind: "refunded"; paymentId: string; amount: number };
function label(status: OrderStatusV2): string {
switch (status.kind) {
case "pending":
return "pending";
case "paid":
return "paid";
default:
return assertNever(status);
}
}
const impossible: OrderStatusV2 = { kind: "paid", paymentId: "PAY-1001", trackingNo: "TRK-778" };
$ npx tsc -p tsconfig.errors.json
errors/05-order-status-errors.ts(15,26): error TS2345: Argument of type '{ kind: "refunded"; paymentId: string; amount: number; }' is not assignable to parameter of type 'never'.
errors/05-order-status-errors.ts(19,74): error TS2353: Object literal may only specify known properties, and 'trackingNo' does not exist in type '{ kind: "paid"; paymentId: string; }'.
C# 14'teki _ kolunun aksine TypeScript yeni case'i derleme zamanında yakalıyor. Narrowing'in ayrıntısı TypeScript Handbook'un Narrowing bölümünde; hataları discriminated union ile modellemek TypeScript Orta serisinin "TypeScript ile Hata Yönetimi: catch'te unknown, Error Narrowing ve Result Pattern" yazısında.
C# 15 ile: union Type'ları
Bu bölüm bir sürüm notu: serinin tabanı C# 14 / .NET 10; aşağıdaki kod ayrı bir projede (csharp15/, kendi global.json'ı ile .NET 11 SDK'ya sabitlenmiş) C# 15 ile derlendi. 10 Kasım 2026'da çıkan C# 15, union anahtar kelimesiyle gerçek bir sum type getiriyor. Microsoft Learn'deki "What's new in C# 15" sayfasına göre özellik net11.0 hedefleyen projelerde varsayılan dil sürümüyle, preview ayarı gerekmeden kullanılabiliyor. Case'ler sıradan tipler; union onları kapalı bir küme olarak birleştiriyor:
public sealed record Pending;
public sealed record Paid(string PaymentId);
public sealed record Shipped(string PaymentId, string TrackingNo);
public sealed record Cancelled(string Reason);
public union OrderStatus(Pending, Paid, Shipped, Cancelled);
// Yalnızca kargoya verilmemiş siparişler iptal edilebilir
public union Cancellable(Pending, Paid);
public static class OrderWorkflow
{
public static Cancelled Cancel(Cancellable status, string reason) => new(reason);
public static string Describe(OrderStatus status) => status switch
{
Pending => "pending",
Paid paid => $"paid with {paid.PaymentId}",
Shipped shipped => $"shipped with {shipped.TrackingNo}",
Cancelled cancelled => $"cancelled: {cancelled.Reason}"
};
}
Paid paid = new("PAY-1001");
OrderStatus[] history = [new Pending(), paid, new Shipped("PAY-1001", "TRK-778"), OrderWorkflow.Cancel(paid, "customer request")];
foreach (OrderStatus status in history)
Console.WriteLine(OrderWorkflow.Describe(status));
$ cd csharp15
$ dotnet run --project Bookstore.Fp.CSharp15
pending
paid with PAY-1001
shipped with TRK-778
cancelled: customer request
Üç fark var. Describe'da _ kolu yok ve derleyici uyarmıyor. Her case tipinden union'a implicit conversion var; paid doğrudan hem OrderStatus array'ine hem Cancellable parametresine verilebiliyor. Ve C# 14'te iki overload ile çözdüğümüz "Pending ya da Paid" artık bir tip. Bir kolu silip kargodaki siparişi iptal etmeye çalışınca:
public static class Broken
{
public static string Short(OrderStatus status) => status switch
{
Pending => "P",
Paid => "$",
Shipped => "S"
};
public static Cancelled TooLate() => OrderWorkflow.Cancel(new Shipped("PAY-1001", "TRK-778"), "too late");
}
OrderStatus.cs(28,62): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Bookstore.Fp.CSharp15.Cancelled' is not covered.
OrderStatus.cs(35,63): error CS1503: Argument 1: cannot convert from 'Bookstore.Fp.CSharp15.Shipped' to 'Bookstore.Fp.CSharp15.Cancellable'
C# 14'teki uyarı "_ ele alınmadı" diyordu; bu kez derleyici eksik case'in adını veriyor. Dokümantasyonun belirttiği bir ayrıntı: derleyicinin ürettiği union bir struct ve default değerinin içi boş (Value null); default bir union'ın switch'e ulaşabileceği yerlerde null kolu eklemek gerekiyor. C# 15 aynı sorunu class hiyerarşileri için de çözen closed modifier'ını da getiriyor. Union ile closed hiyerarşi arasındaki seçim, serialization ve veritabanı etkileri serinin sekizinci yazısının konusu.
Sık Düşülen Tuzaklar
Serialization'ı unutmak. Sum type'ı bir API'den döndürdüğünde ya da bir queue'ya yazdığında, okuyan tarafın hangi case'i aldığını bilmesi gerekir. System.Text.Json bunu [JsonPolymorphic] ve [JsonDerivedType] attribute'larıyla çözüyor; kind adında bir discriminator property'si yazıyor ve okurken ona bakıyor:
JsonSerializerOptions options = new(JsonSerializerDefaults.Web);
OrderStatus[] statuses = [new Pending(), new Paid("PAY-1001"), new Shipped("PAY-1001", "TRK-778"), new Cancelled("customer request")];
string json = JsonSerializer.Serialize(statuses, options);
Console.WriteLine(json);
OrderStatus[] back = JsonSerializer.Deserialize<OrderStatus[]>(json, options)!;
Console.WriteLine($"round trip: {back.SequenceEqual(statuses)}, last: {back[^1]}");
$ dotnet run --project src/Bookstore.Fp.Demo -- 05-json
[{"kind":"pending"},{"kind":"paid","paymentId":"PAY-1001"},{"kind":"shipped","paymentId":"PAY-1001","trackingNo":"TRK-778"},{"kind":"cancelled","reason":"customer request"}]
round trip: True, last: Cancelled { Reason = customer request }
Attribute'lar olmadan OrderStatus abstract olduğu için deserialize edilemez; yeni bir case ekleyip [JsonDerivedType]'ı unutursan serialize sırasında exception alırsın. Discriminator adını TypeScript tarafıyla aynı seçmek (burada kind) iki tarafın aynı JSON'u konuşmasını sağlıyor; C#'ın ürettiği bu JSON'u TypeScript'teki describe fonksiyonu olduğu gibi okuyor:
const statuses = JSON.parse(json) as OrderStatus[];
for (const status of statuses) {
console.log(describe(status));
}
const shipped = statuses[2];
if (shipped.kind === "shipped") {
console.log("narrowed:", shipped.trackingNo);
}
$ node src/05-order-status.ts
pending
paid with PAY-1001
shipped with TRK-778
cancelled: customer request
narrowed: TRK-778
Buradaki as OrderStatus[] bir doğrulama değil, bir iddia: sunucu beklenmeyen bir kind gönderirse TypeScript bunu yakalamaz. Dış dünyadan gelen JSON'u bir schema ile doğrulamak (Zod gibi) ayrı bir adım.
Veritabanında saklamayı unutmak. Entity Framework Core (EF Core; .NET'in veritabanı erişim kütüphanesi) bir record hiyerarşisini TPH (Table-per-Hierarchy; bütün alt tiplerin bir discriminator kolonuyla tek tabloda tutulması) ile saklayabilir, ama bu kez de bütün case'lerin property'leri aynı tabloda nullable kolonlar olarak yan yana durur; yani bayraklı modelin veritabanındaki karşılığı geri gelir. Diğer seçenek TPT (Table-per-Type; her alt tipin ayrı bir tabloda tutulması) daha az nullable kolon ama daha çok join demek. Seçenekler EF Core serisinin "EF Core'da Inheritance Mapping: TPH, TPT ve Hangisini Ne Zaman Seçmeli" yazısında. Sık kullanılan pratik çözüm, entity'de durum için bir kolon ve case'e ait veriyi ayrı kolonlarda tutup sum type'ı domain katmanında kurmak.
_ kolunda sessiz bir varsayılan döndürmek. _ => "unknown" derleyiciyi susturur ama yeni bir case eklendiğinde yanlış ekran metni üretmeye devam eder. C# 14'te UnreachableException fırlat ve case listesini test et; C# 15'e geçtiğinde _ kolunu kaldırıp uyarıyı geri kazan.
Bayrakları geri sızdırmak. Sum type'a geçtikten sonra "kolaylık olsun" diye bool IsShipped => this is Shipped; gibi property'ler eklemek zararsız görünür, ama bir süre sonra kod yine bayraklarla konuşmaya başlar ve pattern matching'in verdiği daraltmayı (shipped.TrackingNo'ya null kontrolsüz erişim) kaybeder. Sorgu gerekiyorsa status is Shipped yazmak zaten yeterince kısa.
Küçük Bir Deneme
★☆☆ OrderStatus'a Delivered(string PaymentId, string TrackingNo, DateTimeOffset DeliveredAt) case'ini ekle. C# 14 projesinde hangi switch'ler sana haber veriyor, hangileri sessizce _ koluna düşüyor? TypeScript'te aynı case'i eklediğinde ne oluyor?
★★☆ OrderWorkflow'a Deliver(Shipped shipped, DateTimeOffset at) geçişini yaz ve kargoya verilmemiş bir siparişi teslim etmeye çalışan bir satırla derleyici hatasını oku.
★★☆ FlaggedOrder'dan OrderStatus'a çeviren bir ToStatus(FlaggedOrder order) fonksiyonu yaz. Tahmin sorusundaki iki imkânsız sipariş için ne döndürmeli? Bu soru, legacy veriyi yeni modele taşırken karşına çıkacak kararın ta kendisi.
★★★ .NET 11 SDK kuruluysa csharp15/ projesinde Cancellable'a benzer bir Shippable union'ı tanımla ve Ship'i onunla yaz. Sonra OrderStatus'u closed bir record hiyerarşisi olarak yeniden yazıp iki yaklaşımın derleyici uyarılarını karşılaştır.
Bir sonraki yazıda "değer yok" durumunu tiplerle ele alıyoruz: Option. Nullable reference type'ların derleyici seviyesinde verdiği korumanın nerede yettiğini, Option<T>'nin nerede fazlasını verdiğini, Map ve Bind ile zincirlenen hesaplamaları ve LINQ'in (Language Integrated Query; C#'ın sorgu yazma özelliği) query syntax'ının neden Option üzerinde de çalışabildiğini göreceğiz.