C# Geliştiricisi için Dart 2: Class'lar, Primary Constructor, Mixin, Sealed Class ve Pattern Matching
Dart'ta sealed bir hiyerarşi üzerindeki switch, yeni bir alt tip eklendiğinde derlenmez ve eksik durumu adıyla gösterir; value equality ise == ve hashCode yazılmadan gelmez. Constructor çeşitlerini, Dart 3.13 primary constructor'larının C# 12'den farkını, class modifier'larını, mixin ve extension type'ları gerçek çıktılarla anlatıyor, Bookstore'un OrderStatus ve Money tiplerini yazıyoruz.
Sürüm notu: Bu yazıdaki Dart örnekleri Dart 3.13.7 (Flutter 3.47.8 ile gelen SDK) ile, Flutter testleri Flutter 3.47.8 ile çalıştırılmıştır. Primary constructor'lar Dart 3.13'te ek ayar gerekmeden açık; 3.12 ve öncesinde derlenmez.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) mobil uygulamasında katalog iki ayrı API çağrısından geliyor: biri "çok satanlar", diğeri "yeni çıkanlar". Dune ikisinde de var. İki yanıtı birleştirip tekrar edenleri atmak için kitapları bir Set'e koyuyorsun:
final a = PlainBook('9780441172719', 'Dune');
final b = PlainBook('9780441172719', 'Dune');
print({a, b}.length);
PlainBook yalnız iki final field'ı olan sıradan bir class. Ekrana 1 mi yazar, 2 mi? İkinci soru: sipariş durumunu dört alt tiple (Pending, Shipped, Delivered, Cancelled) modelledin ve her yerde switch ile etiket üretiyorsun. Ekip beşinci bir durum, Returned, eklediğinde bunu ilk kim fark eder: derleyici mi, test mi, yoksa kullanıcı mı?
Constructor Çeşitleri: Generative, Named, Factory ve Const
Bir önceki yazıda this.isbn ile field atayan basit constructor'ı gördük. Dart'ta constructor'ın birkaç çeşidi var ve C#'tan farklı olarak bir class'ta aynı adla birden fazla constructor overload'u yazamazsın; farklı kurulum yolları named constructor olur:
// Constructor çeşitleri: generative, named, initializer list, const, factory, primary (yazı 04).
class Category {
// Initializer list ve assert içeren generative constructor
Category(String name)
: name = name.trim(),
slug = name.trim().toLowerCase().replaceAll(' ', '-'),
assert(name.trim().isNotEmpty);
// Ana constructor'a yönlendiren named constructor
Category.uncategorized() : this('Uncategorized');
// Factory constructor: cache'teki instance'ı döndürebilir
factory Category.cached(String name) =>
_cache.putIfAbsent(name, () => Category(name));
static final _cache = <String, Category>{};
final String name;
final String slug;
}
// Primary constructor (Dart 3.13): field'lar ve constructor class başlığında
class const Publisher(final String name, {final String country = 'GB'});
class Warehouse(final String code, int capacity) {
// Normal parametreler field initializer'larında görünür
final int reservedForReturns = capacity ~/ 10;
this : assert(code.length == 3);
}
// Private named parameter (Dart 3.12): çağıran `items:` yazar, field private kalır
class Cart {
Cart({required this._items});
final List<String> _items;
int get count => _items.length;
}
void main() {
final scifi = Category(' Science Fiction ');
print('${scifi.name} -> ${scifi.slug}');
print(Category.uncategorized().slug);
print(
'cached: ${identical(Category.cached('Classics'), Category.cached('Classics'))}',
);
const penguin = Publisher('Penguin');
print(
'${penguin.name}, ${penguin.country}, const: ${identical(penguin, const Publisher('Penguin'))}',
);
final ist = Warehouse('IST', 5000);
print('${ist.code} reserves ${ist.reservedForReturns}');
print('cart: ${Cart(items: ['Dune', 'Emma']).count}');
}
Science Fiction -> science-fiction
uncategorized
cached: true
Penguin, GB, const: true
IST reserves 500
cart: 2
Category'nin ana constructor'ında : sonrasındaki kısım initializer list: field'lar constructor gövdesi çalışmadan önce burada atanır. final field'lara gövdede değer veremezsin, initializer list bu yüzden var. C#'ta readonly field'a constructor gövdesinde atama yapabiliyorsun; Dart'ta object gövde çalışmadan önce tamamen kurulmuş olmak zorunda.
Category.uncategorized() named constructor; C#'ta bunun için genelde static bir factory method yazarsın (Category.Uncategorized()). : this(...) ile ana constructor'a yönlendiriyor, C#'taki : this(...) zinciriyle aynı fikir. factory constructor ise yeni bir instance oluşturmak zorunda değil: cache'ten var olanı, hatta bir alt tipin instance'ını döndürebilir. Çağıran taraf farkı görmez, Category.cached('Classics') sıradan bir constructor çağrısı gibi yazılıyor ve çıktıdaki cached: true iki çağrının aynı object'i döndürdüğünü gösteriyor.
Initializer list'teki assert'e dikkat. Dart'ta assert'ler yalnız geliştirmede çalışır: flutter run'ın debug modunda ve flutter test'te açıktır, release build'de kod hiç yoktur. dart run ise varsayılan olarak assert'leri çalıştırmaz. Warehouse('ISTANBUL', 10) ile denediğimizde düz dart run sessizce geçti, dart run --enable-asserts ise durdu:
Unhandled exception:
'file:///home/user/src/bookstore-flutter/scratch/bin/a.dart': Failed assertion: line 2 pos 17: 'code.length == 3': warehouse code must have 3 letters
Kural basit: assert geliştiriciye yönelik bir kontrol; kullanıcıdan gelen veriyi doğrulamak için exception fırlatan gerçek bir kontrol yazarsın.
Dart 3.13 Primary Constructor'ları ve C# 12 ile Farkı
Publisher ve Warehouse, Dart 3.13 ile gelen primary constructor syntax'ını kullanıyor. Parametre listesi class adının hemen yanında. final ya da var ile işaretlenen parametreler aynı adla bir field tanımlıyor; Publisher'ın gövdesi boş olduğu için {} yerine ; yeterli. class const Publisher(...) constructor'ı const yapıyor, çıktıdaki const: true bunun sonucu.
C# 12'nin primary constructor'larıyla ilk bakışta aynı görünüyor, ama anlamı farklı. C# serisindeki "C# 12 Yenilikleri: Primary Constructor ve Collection Expression" yazısında anlatıldığı gibi C#'ta class Warehouse(string code, int capacity) parametreleri field yapmaz; class içinde kullanılan parametreler derleyicinin ürettiği gizli bir yere yakalanır ve üstelik değiştirilebilir. Dart'ta hangi parametrenin field olacağına sen karar verirsin: final String code immutable bir field, int capacity ise yalnız kurulum sırasında görünen düz bir parametre. reservedForReturns field'ı capacity'yi doğrudan initializer'ında kullanabiliyor; assert ve initializer list ise gövdedeki this : satırına taşınıyor.
Cart'taki this._items Dart 3.12'nin private named parameter'ı. Dart'ta _ ile başlayan isimler library'ye private'tır; eskiden private bir field'ı named parametreyle doldurmak için parametreye public bir ad verip initializer list'te atama yapman gerekiyordu. Artık çağıran taraf items: yazıyor, field _items olarak kalıyor. Flutter widget'larında state'i dışarıya açmadan constructor'dan almak için kullanışlı.
Her Class Bir Interface: implements ve Class Modifier'ları
Dart'ta ayrı bir interface anahtar kelimesiyle tanımlanan tip yok; her class aynı zamanda bir interface. Başka bir class onu implements ile uygularsa kodunu almaz, yalnız sözleşmesini üstlenir ve her member'ı kendisi yazar. Test için sahte bir saat yazmak bu yüzden tek satırlık bir iş:
// Implicit interface'ler, class modifier'ları, mixin'ler, extension method'lar ve extension type'lar (yazı 04).
// Her class aynı zamanda bir interface'tir: bir fake, kod miras almadan `implements` edebilir
class Clock {
DateTime now() => DateTime.now().toUtc();
}
class FixedClock implements Clock {
FixedClock(this.fixed);
final DateTime fixed;
@override
DateTime now() => fixed;
}
// interface class: library dışında implement edilebilir, extend edilemez
interface class PaymentGateway {
bool charge(int kurus) => kurus > 0;
}
// Mixin, base class olmadan davranış ekler
mixin Auditable {
final List<String> log = [];
void audit(String message) => log.add(message);
}
class StockItem with Auditable {
StockItem(this.isbn, this.quantity);
final String isbn;
int quantity;
void reserve(int count) {
quantity -= count;
audit('reserved $count of $isbn, left $quantity');
}
}
// Extension method: C#'taki static extension method gibi
extension on DateTime {
String toShortDate() =>
'${year.toString().padLeft(4, '0')}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
}
// Extension type: int'in etrafında maliyetsiz bir sarmalayıcı, yalnız derleme anında kontrol edilir
extension type const Money(int kurus) {
Money operator +(Money other) => Money(kurus + other.kurus);
Money operator *(int quantity) => Money(kurus * quantity);
String format() =>
'${kurus ~/ 100}.${(kurus % 100).toString().padLeft(2, '0')} TRY';
}
void main() {
final clock = FixedClock(DateTime.utc(2026, 11, 15, 9, 30));
print('today: ${clock.now().toShortDate()}');
print('charge: ${PaymentGateway().charge(48900)}');
final dune = StockItem('9780441172719', 12)..reserve(2);
print(dune.log);
const price = Money(48900);
final total = price * 2 + const Money(21500);
print('total: ${total.format()}');
print(
'at run time: ${total.runtimeType}, is int: ${(total as Object) is int}',
);
}
today: 2026-11-15
charge: true
[reserved 2 of 9780441172719, left 10]
total: 1193.00 TRY
at run time: int, is int: true
C#'ta test için bir IClock interface'i ve ondan türeyen iki class yazardın. Dart'ta Clock'un kendisi sözleşme; FixedClock onu implement ediyor. Flutter'ın resmî mimari rehberinin "mock yerine fake" önerisi de bu özelliğe dayanıyor: test yazısında repository'lerin fake'lerini aynı yolla yazacağız.
Dart 3.0, bir class'ın kendi library'si dışında nasıl kullanılabileceğini sınırlayan modifier'lar getirdi. Bir library kabaca bir dosya; kısıtlar aynı dosyanın içinde değil, başka dosyalardan kullanımda devreye giriyor:
| Modifier | Instance oluşturulur mu | extends | implements | C#'taki yakın karşılık |
|---|---|---|---|---|
| (yok) | Evet | Evet | Evet | Sıradan class |
abstract | Hayır | Evet | Evet | abstract class |
interface | Evet | Hayır | Evet | Interface + varsayılan implementasyon |
base | Evet | Evet | Hayır | Implement edilemeyen base class |
final | Evet | Hayır | Hayır | sealed class (C# anlamında) |
sealed | Hayır | Hayır | Hayır | C# 15 closed hierarchy |
İsim çakışmasına dikkat: C#'ın sealed'ı "türetilemez" demek ve Dart'taki karşılığı final. Dart'ın sealed'ı ise "alt tipleri yalnız bu library'de tanımlanabilir" demek ve derleyiciye bütün alt tipleri bildiği bir hiyerarşi veriyor. Bu, birazdan göreceğimiz exhaustiveness kontrolünün temeli.
Mixin, Extension Method ve Extension Type
mixin, bir base class'tan türemeden davranış eklemenin yolu. StockItem with Auditable diyerek log field'ını ve audit method'unu aldık. C#'ta en yakın karşılık interface'lerin default implementation'ı, ama C# interface'i state tutamaz; mixin'in kendi field'ı olabilir. Flutter'da en sık gördüğün mixin SingleTickerProviderStateMixin: animasyon kullanan bir State class'ına ticker desteğini bu yolla eklersin.
Extension method C#'takiyle aynı fikir: var olan bir tipe, kaynak koduna dokunmadan method eklemek. Syntax farklı: C#'ta this parametreli static bir method yazarsın, Dart'ta extension on DateTime { ... } bloğu açarsın. İsimsiz extension yalnız tanımlandığı dosyada görünür.
Extension type ise Dart 3.3'ün getirdiği, C#'ta doğrudan karşılığı olmayan bir şey. Money, derleme anında ayrı bir tip: int bekleyen yere Money, Money bekleyen yere düz bir int veremezsin, ve sadece tanımladığın operatörler kullanılabilir. Çalışma anında ise ortada hiçbir sarmalayıcı object yok; çıktıdaki at run time: int, is int: true bunu gösteriyor. C#'ta bunun için bir readonly record struct Money(long Kurus) yazardın; extension type aynı tip güvenliğini sıfır bellek maliyetiyle veriyor. Bedeli de bu: çalışma anında tip yok, is Money kontrolü is int'ten farksız.
Sealed Class ve Pattern Matching: OrderStatus
Sipariş durumunu bookstore_app'te sealed bir hiyerarşiyle modelledik:
sealed class OrderStatus {
const OrderStatus();
}
final class Pending extends OrderStatus {
const Pending();
}
final class Shipped extends OrderStatus {
const Shipped({required this.trackingNumber, required this.shippedAt});
final String trackingNumber;
final DateTime shippedAt;
}
final class Delivered extends OrderStatus {
const Delivered({required this.deliveredAt});
final DateTime deliveredAt;
}
final class Cancelled extends OrderStatus {
const Cancelled({this.reason});
final String? reason;
}
/// Sipariş geçmişinde gösterilen etiket; switch her alt tipi kapsamak zorunda.
String statusLabel(OrderStatus status, {required DateTime now}) =>
switch (status) {
Pending() => 'Waiting for payment',
Shipped(:final trackingNumber, :final shippedAt)
when now.difference(shippedAt).inDays > 7 =>
'Delayed in shipping ($trackingNumber)',
Shipped(:final trackingNumber) => 'On the way ($trackingNumber)',
Delivered(:final deliveredAt) =>
'Delivered on ${deliveredAt.toIso8601String().substring(0, 10)}',
Cancelled(reason: null) => 'Cancelled',
Cancelled(:final reason?) => 'Cancelled: $reason',
};
switch burada bir ifade (switch expression), C# 8'den tanıdık. Her satır bir pattern: Shipped(:final trackingNumber) object pattern'i hem tipi kontrol ediyor hem de getter'ı aynı adlı bir değişkene açıyor. when guard'ı C#'taki when ile aynı; yedi günden uzun süredir yolda olan kargoyu ayrı bir etiketle gösteriyor. Cancelled(reason: null) sabit bir değerle eşleşiyor, :final reason? ise null olmayan değeri yakalıyor ve reason'ı String olarak bağlıyor. Testteki altı durumun hepsi beklenen etiketi verdi:
00:00 +1: test/domain/models/order_status_test.dart: each status has a label
Şimdi "Önce Tahmin Et"in ikinci sorusu. Aynı dosyaya final class Returned extends OrderStatus ekleyip switch'e dokunmadık:
error - lib/order_status.dart:31:5 - The type 'OrderStatus' isn't exhaustively matched by the switch cases since it doesn't match the pattern 'Returned()'. Try adding a wildcard pattern or cases that match 'Returned()'. - non_exhaustive_switch_expression
Derleyici fark ediyor, kod derlenmiyor. OrderStatus sealed olduğu için derleyici bütün alt tiplerini biliyor ve hangisinin eksik kaldığını adıyla söylüyor. Bunun için switch'te _ => gibi bir varsayılan dal olmaması gerekiyor; varsayılan dal yazarsan yeni durum oraya düşer ve hata kaybolur. C#'ta aynı güvenceye C# 15'in union type'ları ve closed hierarchy'leriyle kavuştuk ("C# 15 Union Types ve Closed Hierarchies" yazısı); daha önceki C# sürümlerinde derleyici yalnız uyarı veriyordu.
Dot Shorthand'ler ve Book'un Yeni Hâli
Bir önceki yazıdaki int price'ı Money extension type'ına taşıdık ve kitabın formatını bir enum ile ekledik:
/// Kuruş cinsinden tutar (1 TRY = 100 kuruş). Çalışma anında düz bir int.
extension type const Money(int kurus) {
static const zero = Money(0);
Money operator +(Money other) => Money(kurus + other.kurus);
Money operator *(int quantity) => Money(kurus * quantity);
String format() =>
'${kurus ~/ 100}.${(kurus % 100).toString().padLeft(2, '0')} TRY';
}
enum BookFormat { paperback, hardcover, ebook }
class Book {
const Book({
required this.isbn,
required this.title,
required this.author,
required this.price,
required this.publishedYear,
this.subtitle,
this.coverUrl,
this.format = .paperback,
});
final String isbn;
final String title;
final String? subtitle;
final Author author;
final Money price;
final int publishedYear;
final String? coverUrl;
final BookFormat format;
String get displayTitle => subtitle == null ? title : '$title: $subtitle';
bool get shipsPhysically => switch (format) {
.paperback || .hardcover => true,
.ebook => false,
};
// Bir kitap, diğer field'lar ne derse desin ISBN'iyle tanımlanır
@override
bool operator ==(Object other) => other is Book && other.isbn == isbn;
@override
int get hashCode => isbn.hashCode;
}
.paperback ve .ebook, Dart 3.10'un dot shorthand'leri. Beklenen tip belli olduğunda (BookFormat tipindeki bir parametrenin varsayılan değeri ya da BookFormat üzerindeki bir switch) BookFormat. önekini yazmana gerek kalmıyor. Flutter'da bunu en çok mainAxisAlignment: .center gibi enum parametrelerinde kullanacağız. .paperback || .hardcover ise iki pattern'i "veya" ile birleştiren logical-or pattern'i.
== ve hashCode: Value Equality Kendiliğinden Gelmez
Book'un sonundaki iki override, "Önce Tahmin Et"in ilk sorusunun cevabı. dart_samples'ta iki class'ı yan yana koyduk:
// Tuzak: override etmezsen == kimlik (identity) karşılaştırır (yazı 04).
class PlainBook {
PlainBook(this.isbn, this.title);
final String isbn;
final String title;
}
class Book {
Book(this.isbn, this.title);
final String isbn;
final String title;
// ISBN aynıysa iki Book object'i aynı kitaptır
@override
bool operator ==(Object other) => other is Book && other.isbn == isbn;
@override
int get hashCode => isbn.hashCode;
}
void main() {
// Aynı kitap iki kez geliyor, örneğin iki API çağrısından
final a = PlainBook('9780441172719', 'Dune');
final b = PlainBook('9780441172719', 'Dune');
print(
'PlainBook a == b: ${a == b}, set size: ${{a, b}.length}, contains: ${[a].contains(b)}',
);
final c = Book('9780441172719', 'Dune');
final d = Book('9780441172719', 'Dune');
print(
'Book c == d: ${c == d}, set size: ${{c, d}.length}, contains: ${[c].contains(d)}',
);
// Record'lar ek kod olmadan değerle karşılaştırılır
print(
'record: ${(isbn: '9780441172719', title: 'Dune') == (isbn: '9780441172719', title: 'Dune')}',
);
}
PlainBook a == b: false, set size: 2, contains: false
Book c == d: true, set size: 1, contains: true
record: true
Override edilmemiş ==, C#'taki class'larda olduğu gibi referans karşılaştırır; set'te Dune iki kez kalıyor, contains bulamıyor. C#'ta record yazınca derleyici Equals ve GetHashCode'u üretir; Dart class'larında böyle bir kısayol yok, ya kendin yazarsın ya da kod üreten bir paket kullanırsın (API yazısında freezed'e bakacağız). =='yi override edip hashCode'u unutmak ise daha sinsi bir hata: == true döner ama Set ve Map önce hash'e baktığı için iki object'i yine ayrı tutar.
Book için bilinçli bir seçim yaptık: eşitlik bütün field'larla değil, ISBN (kitapların uluslararası kimlik numarası) ile. Bir kitap domain'de bir entity; fiyatı değişse de aynı kitap. Testte bunu açıkça yazdık: 40. yıl baskısının farklı başlık ve fiyatıyla gelen Dune, katalogdaki Dune'a eşit ve set'te tek kalıyor.
00:00 +4: test/domain/models/book_test.dart: books are equal when the ISBN matches
00:00 +5: test/domain/models/book_test.dart: only ebooks skip shipping
00:00 +6: test/widget_test.dart: tapping the button adds a book to the cart
00:00 +7: All tests passed!
Üretimde Dikkat
Sealed hiyerarşilerde switch'e varsayılan dal (_ =>) eklemeyi alışkanlık hâline getirme. Exhaustiveness kontrolünün değeri, yeni bir alt tip eklendiğinde derleyicinin seni her kullanım yerine götürmesinde; varsayılan dal bu kontrolü kapatır. API'den bilinmeyen bir durum gelme ihtimaline karşı ise JSON'dan model oluşturulan yerde açık bir UnknownStatus alt tipi tanımla.
Extension type'lar API sınırında koruma sağlamaz. JSON'dan gelen bir int'i Money(...) ile sarmak derleme anında tip güvenliği verir, ama çalışma anında dynamic bir değer üzerinden as Money yaptığında hiçbir kontrol yapılmaz. Dönüşümü tek bir yerde, açıkça yap.
Primary constructor'lar Dart 3.13 istiyor. Paylaşılan bir paket yazıyorsan, bu syntax'ı kullanmak pubspec.yaml'daki SDK alt sınırını 3.13'e çeker ve daha eski Flutter sürümündeki kullanıcıların paketi alamamasına yol açar. Uygulama kodunda sorun değil, kütüphane kodunda karar.
Sık Düşülen Tuzaklar
Birinci tuzak, C#'taki gibi constructor overload'u yazmaya çalışmak. Dart'ta aynı adla ikinci bir constructor derlenmez; named constructor (Book.fromJson, Category.uncategorized) ya da opsiyonel named parametreler kullanırsın.
İkinci tuzak, final field'a constructor gövdesinde değer atamaya çalışmak. Dart'ta final field ya tanımında, ya initializer list'te, ya da this.x parametresiyle atanır; gövdeye gelindiğinde object kurulmuştur.
Üçüncü tuzak, Dart'ın sealed'ını C#'ın sealed'ı sanmak. C# anlamındaki "türetilemez" Dart'ta final; Dart'ın sealed'ı exhaustiveness için kapalı bir hiyerarşi kurar ve kendisinden instance oluşturulamaz.
Dördüncü tuzak, =='yi override edip hashCode'u unutmak ya da hashCode'u değişebilen bir field'dan hesaplamak. Bir object set'e girdikten sonra hash'i değişirse set onu bir daha bulamaz.
Küçük Bir Deneme
★☆☆ OrderStatus'a Returned alt tipini ekle, derleyicinin gösterdiği yere bir etiket yaz ve order_status_test.dart'a bu durumu ekle.
★★☆ Money'ye operator - ve bir isNegative getter'ı ekle. Sonra Money price = 48900; yazmayı dene: derleyici ne diyor? Aynı satırı C#'ta readonly record struct Money(long Kurus) ile yazsaydın ne olurdu?
★★★ Category'yi primary constructor'a çevir. name ve slug field'larını korurken trim ve slug hesaplamasını nereye koyman gerekiyor? uncategorized ve cached constructor'larını yeni syntax'la (new uncategorized(), factory cached(...)) yaz ve çıktının değişmediğini doğrula.
Bir sonraki yazıda Dart'ın asenkron modeline geçiyoruz: tek thread'li event loop, Future ile Task arasındaki farklar, Stream ile IAsyncEnumerable, UI'ı dondurmadan büyük bir JSON'u parse etmek için Isolate.run ve await edilmeyen bir Future'ın hatasının nasıl kaybolduğu.