C# Geliştiricisi için Dart 1: Tipler, Null Safety, Collection'lar, Function'lar ve Record'lar
Dart'ın null safety'si C#'tan farklı olarak sound: nullable bir değeri kontrol etmeden kullanmak uyarı değil derleme hatası, ve non-nullable bir değişken çalışma anında da null olamaz. C# bilen okur için var/final/const farkını, const'un canonical instance üretmesini, collection if/for ve null-aware element'leri, named parametreleri ve record'ları gerçek çıktılarla anlatıyor, Bookstore'un Book ve Author modellerini 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. Karşılaştırma için kullanılan C# kodu .NET 10 SDK 10.0.404 ile derlenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) mobil uygulamasını yazmadan önce Dart'ı C# bilen biri gözüyle tanıyoruz. Bu yazı dilin ilk yarısı: tipler, null safety, collection'lar, function'lar ve record'lar. Bir sonraki yazı class'lar ve pattern matching ile devam ediyor.
İki küçük parça. Birincisi C# ile neredeyse aynı görünüyor:
String? findSubtitle(String isbn) => null;
void main() {
String? subtitle = findSubtitle('9780441172719');
print(subtitle.length);
}
C#'ta Nullable açıkken aynı kod derlenir mi? Dart'ta derlenir mi? İkincisi:
const a = Author('Jane Austen');
const b = Author('Jane Austen');
final c = Author('Jane Austen');
identical(a, b) ve identical(a, c) ne döndürür? İkisini de birazdan gerçek çıktıyla göreceğiz.
var, final ve const: C#'taki Karşılıkları
Örnekler serinin deposundaki dart_samples paketinde. İlk dosya değişken tanımlarını ve null safety'yi topluyor:
// var / final / const, sound null safety ve late (yazı 03).
class Author {
const Author(this.name);
final String name;
}
String? findSubtitle(String isbn) =>
isbn == '9780141439549' ? 'A Study of Provincial Life' : null;
late final String apiBaseUrl;
void main() {
var title = 'Dune'; // tipi String olarak çıkarılır
title = 'Emma'; // var yeniden atanabilir
final createdAt = DateTime.utc(2026, 11, 15); // bir kez, çalışma anında atanır
const pageSize = 20; // derleme anı sabiti
print('$title, $createdAt, $pageSize');
// const object'ler canonical'dır: aynı değerler, aynı instance
const a = Author('Jane Austen');
const b = Author('Jane Austen');
final c = Author('Jane Austen');
print(
'identical(a, b): ${identical(a, b)}, identical(a, c): ${identical(a, c)}',
);
// final list: değişken sabit, list değil
final cart = ['Dune'];
cart.add('Emma');
print('final list: $cart');
const featured = ['Dune', 'Emma'];
try {
featured.add('Middlemarch');
} on UnsupportedError catch (e) {
print('const list: $e');
}
// Null safety
String? subtitle = findSubtitle('9780441172719');
print('length: ${subtitle?.length}, or: ${subtitle ?? '(no subtitle)'}');
subtitle ??= 'Untitled';
print('after ??= : $subtitle');
final middlemarch = findSubtitle('9780141439549')!; // "null olmadığını biliyorum"
print('bang: $middlemarch');
try {
print(apiBaseUrl);
} on Error catch (e) {
print('late: ${e.runtimeType}: $e');
}
apiBaseUrl = 'http://127.0.0.1:52813';
print('late after set: $apiBaseUrl');
// Fiyatlar neden double değil, kuruş cinsinden int
print('double: ${0.1 + 0.2}, ${4.35 * 100}');
print('int kurus: ${10 + 20}');
}
dart run bin/variables.dart
Emma, 2026-11-15 00:00:00.000Z, 20
identical(a, b): true, identical(a, c): false
final list: [Dune, Emma]
const list: Unsupported operation: Cannot add to an unmodifiable list
length: null, or: (no subtitle)
after ??= : Untitled
bang: A Study of Provincial Life
late: LateError: LateInitializationError: Field 'apiBaseUrl' has not been initialized.
late after set: http://127.0.0.1:52813
double: 0.30000000000000004, 434.99999999999994
int kurus: 30
var C#'taki gibi: tip derleyici tarafından çıkarılır ve sonra değişmez, yalnız değer değişir. final C#'ta yerel değişkenler için doğrudan bir karşılığı olmayan şey: bir kez atanır, sonra atanamaz. Field'larda readonly'ye denk gelir. Dart'ta yerel değişkenler için de yaygın kullanılır; isteğe bağlı prefer_final_locals lint kuralını açarsan analyzer değişmeyen yerel değişkenlerde final ister.
const ise C#'tan çok daha güçlü. C#'ta const yalnız sayı, string ve benzeri basit tipler için var. Dart'ta bir class'ın constructor'ı const ise o class'tan derleme anında object oluşturabilirsin. Üstelik derleyici bu object'leri canonical yapar: aynı değerlerle oluşturulan iki const object aslında tek bir instance'tır. "Önce Tahmin Et"in ikinci cevabı bu yüzden true ve false: a ile b aynı object, c ise final ile çalışma anında oluşturulmuş ayrı bir object.
Bunun Flutter'da somut bir karşılığı var: const Text('Bookstore') gibi bir widget her build'de yeniden oluşturulmaz, Flutter onu değişmemiş bilir ve o alt ağacı yeniden build etmez. Altıncı yazıda bunu ölçeceğiz. Bir de final ile const list farkına dikkat: final list'e eleman eklenebilir, çünkü sabit olan değişkendir; const list'in kendisi değiştirilemez ve add çalışma anında UnsupportedError fırlatır.
Sound Null Safety: Uyarı Değil, Hata
Dart'ta String null olamaz; null olabilen tip String?. C# 8'den beri gelen nullable reference types aynı syntax'ı kullanıyor, ama davranış farklı. "Önce Tahmin Et"teki ilk parçayı her iki dilde derledik. Dart:
dart analyze
error - bin/null_error.dart:5:18 - The property 'length' can't be unconditionally accessed because the receiver can be 'null'. Try making the access conditional (using '?.') or adding a null check to the target ('!'). - unchecked_use_of_nullable_value
error - bin/null_error.dart:6:15 - A value of type 'Null' can't be assigned to a variable of type 'int'. Try changing the type of the variable, or casting the right-hand type to 'int'. - invalid_assignment
İkinci hata, dosyaya eklediğimiz int stock = null; satırından geliyor. dart run bu dosyayı hiç çalıştırmıyor, derleme hatasıyla duruyor. Aynı kodun C# hâli <Nullable>enable</Nullable> açık bir .NET 10 projesinde:
string? subtitle = FindSubtitle("9780441172719");
Console.WriteLine(subtitle.Length);
static string? FindSubtitle(string isbn) => null;
Program.cs(2,19): warning CS8602: Dereference of a possibly null reference.
Build succeeded.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
"Önce Tahmin Et"in ilk cevabı bu: C# derliyor, uyarı veriyor ve kod çalışma anında NullReferenceException ile düşüyor; Dart derlemiyor. C#'ın nullable annotation'ları derleyiciye ipucu, çalışma anında bir garanti yok. Dart'ın null safety'si "sound": tipi String olan bir değişken çalışma anında da hiçbir zaman null değildir ve derleyici bu bilgiyle gereksiz null kontrollerini atabilir. Dart 3'ten beri null safety'siz kod derlenmiyor; C#'taki gibi proje bazında açıp kapatılan bir ayar değil. C# serisindeki "C# Nullable: int?, string? ve null'ı Tasarımla Yönetmek" yazısında anlatılan disiplin burada derleyicinin zorunluluğu.
Operatörler C#'tan tanıdık. ?. null ise null döner, ?? null ise sağdakini verir, ??= null ise atar. ! C#'taki null-forgiving operatörüne benzer ama bir farkla: C#'ta ! yalnız uyarıyı susturur, Dart'ta çalışma anında kontrol yapar ve değer null ise hemen hata fırlatır.
late ise "bu değişkene değeri sonra vereceğim, ondan önce okumayacağım" sözü. Derleyici kontrolü çalışma anına erteler; sözü tutmazsan çıktıdaki LateInitializationError'ı alırsın. late'i dependency injection ile sonradan verilen değerler ya da initState'te kurulan controller'lar için kullanırsın. Her null hatasını late ile susturmak ise sound null safety'nin sana verdiği garantiyi çalışma anı hatasına geri çevirir.
Sayılar ve Para: int, double ve Neden Kuruş
Dart'ta iki sayı tipi var: int ve double. Mobilde ve desktop'ta int 64 bitlik tam sayı, yani C#'taki long. Web'de ise JavaScript'in sayı tipine derlenir ve yalnız 53 bite kadar kesin kalır; büyük ID'leri web'de string olarak taşımak bu yüzden daha güvenli. double IEEE (Elektrik ve Elektronik Mühendisleri Enstitüsü) 754 standardındaki çift duyarlıklı kayan nokta sayısı, C#'taki double ile aynı.
Dart'ın çekirdek kütüphanesinde C#'taki decimal'in karşılığı yok. Bookstore API'si fiyatları decimal tutuyor; mobil tarafta fiyatı double'a koymak çıktıdaki sonucu verir: 0.1 + 0.2 0.30000000000000004, 4.35 * 100 434.99999999999994. Sepet toplamında bu tür kuruş kaymaları kullanıcıya yanlış tutar göstermek demek. Seride parayı kuruş cinsinden int olarak tutuyoruz: 489,00 TL 48900 olarak saklanıyor, yalnız ekranda gösterirken biçimlendiriliyor. Bir sonraki yazıda bunu bir extension type ile tip güvenli hâle getireceğiz. Ondalık hesap gerçekten gerekiyorsa pub.dev'de decimal paketi var.
Collection'lar: List, Set, Map ve Collection if/for
Dart'ın üç temel collection'ı List, Set ve Map; C#'taki List<T>, HashSet<T> ve Dictionary<TKey, TValue>'ın karşılığı. Asıl fark literal'lerin içinde kod yazabilmek:
// List / Set / Map, collection if/for, spread, null-aware element'ler, digit separator'lar, wildcard'lar (yazı 03).
void main() {
final titles = <String>['Dune', 'Emma', 'Dune'];
final unique = <String>{...titles}; // Set tekrar edeni atar
final stock = <String, int>{'Dune': 12, 'Emma': 0};
print('list: $titles, set: $unique, map: $stock');
// collection if ve collection for list'i yerinde kurar
const showOutOfStock = false;
final visible = [
for (final entry in stock.entries)
if (showOutOfStock || entry.value > 0) '${entry.key} (${entry.value})',
];
print('visible: $visible');
// spread ve null-aware spread
List<String>? recentlyViewed;
final menu = ['Catalog', ...?recentlyViewed, 'Cart'];
print('menu: $menu');
// null-aware element (Dart 3.8): yalnız null değilse ekle
String? subtitle;
const coupon = 'WELCOME10';
final chips = ['Dune', ?subtitle, ?couponOrNull(coupon)];
print('chips: $chips');
// digit separator (Dart 3.6)
const freeShippingLimit = 100_000; // kuruş cinsinden 1.000,00 TL
print('limit: $freeShippingLimit');
// wildcard (Dart 3.7): `_` bağlanmaz, bu yüzden tekrar edebilir
print(withSeparators(['Dune', 'Emma', 'Middlemarch'], (_, _) => '---'));
}
String? couponOrNull(String code) => code.isEmpty ? null : code;
List<String> withSeparators(
List<String> titles,
String Function(String title, int index) separator,
) => [
for (var i = 0; i < titles.length; i++) ...[
titles[i],
if (i < titles.length - 1) separator(titles[i], i),
],
];
list: [Dune, Emma, Dune], set: {Dune, Emma}, map: {Dune: 12, Emma: 0}
visible: [Dune (12)]
menu: [Catalog, Cart]
chips: [Dune, WELCOME10]
limit: 100000
[Dune, ---, Emma, ---, Middlemarch]
Collection if ve for, C#'ta LINQ (C#'ın collection'lar üzerinde sorgu yazma özelliği) ile ya da ayrı bir döngüyle yapacağın işi literal'in içinde yapıyor. Flutter'da bunu sürekli kullanacağız: bir Column'un children list'ine koşula bağlı bir widget eklemek ya da bir list'ten widget üretmek tam olarak bu syntax. ... (spread) başka bir collection'ı açıp ekler, ...? ise collection null ise hiçbir şey eklemez.
Dart 3.8'in null-aware element'i ?subtitle, "null değilse ekle" demenin en kısa yolu; önceden bunun için if (subtitle != null) subtitle yazıyorduk. Dart 3.6'nın digit separator'ı 100_000, C#'taki 100_000 ile aynı. Dart 3.7'nin wildcard'ı _ ise C#'taki discard'a benzer: değeri bağlamaz, bu yüzden aynı parametre listesinde birden fazla _ yazabilirsin. Flutter'da en sık gördüğün yer (_, _) => const Divider() gibi callback'ler.
Function'lar: Named, Optional ve required Parametreler
// Named ve optional parametreler, arrow function'lar, closure'lar ve record'lar (yazı 03).
String formatPrice(
int kurus, {
String currency = 'TRY',
bool showCurrency = true,
}) {
final value = '${kurus ~/ 100}.${(kurus % 100).toString().padLeft(2, '0')}';
return showCurrency ? '$value $currency' : value;
}
String describe(String title, {required int year, String? subtitle}) =>
subtitle == null ? '$title ($year)' : '$title: $subtitle ($year)';
String shortTitle(String title, [int maxLength = 10]) =>
title.length <= maxLength ? title : '${title.substring(0, maxLength)}...';
// Closure, oluşturulduğu scope'taki değişkenleri tutar.
int Function() counter() {
var count = 0;
return () => ++count;
}
// Record isimsiz, immutable ve yapısal eşitliği olan bir değerdir.
({int items, int total}) summarize(List<(String, int)> lines) {
var total = 0;
for (final (_, price) in lines) {
total += price;
}
return (items: lines.length, total: total);
}
void main() {
print(formatPrice(48900));
print(formatPrice(48900, showCurrency: false));
print(
describe('Middlemarch', year: 1871, subtitle: 'A Study of Provincial Life'),
);
print(shortTitle('The Hobbit, or There and Back Again'));
final next = counter();
next();
next();
print('counter: ${next()}');
final summary = summarize([('Dune', 48900), ('Emma', 21500)]);
print('summary: $summary');
final (:items, :total) = summary; // field adıyla destructuring
print('items=$items total=${formatPrice(total)}');
print('records equal: ${(items: 2, total: 70400) == summary}');
print('runtimeType: ${summary.runtimeType}');
}
489.00 TRY
489.00
Middlemarch: A Study of Provincial Life (1871)
The Hobbit...
counter: 3
summary: (items: 2, total: 70400)
items=2 total=704.00 TRY
records equal: true
runtimeType: ({int items, int total})
C#'ta her parametre zorunludur, varsayılan değer verdiklerin hariç; named argument ise çağıran tarafın tercihidir. Dart'ta iki ayrı tür var. {} içindeki parametreler named'dır ve çağırırken adıyla verilmek zorundadır; varsayılan olarak opsiyoneldir, zorunlu olması için required yazarsın. [] içindeki parametreler ise opsiyonel positional parametrelerdir. Flutter'ın API'si neredeyse tamamen named parametrelerle kurulu: Text('Dune', style: ..., maxLines: 1). Constructor'larda required görmeye bu yüzden alışacaksın.
=> tek ifadelik gövde için, C#'taki expression-bodied member ile aynı. Function'lar first-class değer: değişkene atanır, parametre olarak verilir, tipi int Function() gibi yazılır; C#'taki Func<int>'in karşılığı. counter() örneğindeki closure, C#'taki lambda'nın yakaladığı değişken gibi çalışıyor: count her çağrıda artıyor ve dışarıdan erişilemiyor.
Record'lar ve Destructuring
Dart 3.0 ile gelen record'lar, birden fazla değeri bir class tanımlamadan taşımanın yolu. (String, int) positional, ({int items, int total}) named field'lı bir record. C#'taki tuple'a benziyorlar ama iki önemli fark var. C#'ın ValueTuple'ı mutable bir struct ve field adları yalnız derleme anında var; Dart record'u immutable ve field adları tipin bir parçası: çıktıdaki runtimeType ({int items, int total}). Eşitlik yapısal: aynı field'lara ve değerlere sahip iki record == ile eşit, çıktıdaki records equal: true bunu gösteriyor.
Destructuring, C#'taki deconstruction'ın karşılığı. final (:items, :total) = summary; named field'ları aynı adlı değişkenlere açıyor; for (final (_, price) in lines) ise positional record'un ilk alanını wildcard ile atlıyor. C# serisindeki record'larla karıştırma: C#'ın record class'ı bir class türü, Dart'ın record'u ise isimsiz bir değer tipi. Dart'ta C#'ın record'una en yakın şey, bir sonraki yazıda göreceğimiz immutable class ve == override'ı.
Bookstore Modellerinin İlk Hâli
Öğrendiklerimizi bookstore_app'e taşıyoruz. Flutter'ın resmî mimari rehberinde domain modelleri lib/domain/models/ altında duruyor:
class Author {
const Author({required this.name, this.birthYear});
final String name;
final int? birthYear;
}
import 'author.dart';
class Book {
const Book({
required this.isbn,
required this.title,
required this.author,
required this.price,
required this.publishedYear,
this.subtitle,
this.coverUrl,
});
final String isbn;
final String title;
final String? subtitle;
final Author author;
/// Fiyat kuruş cinsinden (1 TRY = 100 kuruş); para hiçbir zaman double değil.
final int price;
final int publishedYear;
final String? coverUrl;
String get displayTitle => subtitle == null ? title : '$title: $subtitle';
}
typedef CartLine = ({Book book, int quantity});
({int itemCount, int total}) summarizeCart(List<CartLine> lines) {
var itemCount = 0;
var total = 0;
for (final (:book, :quantity) in lines) {
itemCount += quantity;
total += book.price * quantity;
}
return (itemCount: itemCount, total: total);
}
String formatTry(int kurus) =>
'${kurus ~/ 100}.${(kurus % 100).toString().padLeft(2, '0')} TRY';
Bütün field'lar final, constructor const: model immutable ve derleme anında oluşturulabiliyor. this.isbn syntax'ı parametreyi doğrudan field'a atıyor; C#'ta constructor gövdesinde yazdığın Isbn = isbn; satırlarının karşılığı. Opsiyonel bilgiler (subtitle, coverUrl, birthYear) nullable. Sepet satırı için bir class yazmadık, typedef ile bir record tipine ad verdik. Testleri:
test('summarizeCart counts items and sums prices in kurus', () {
final summary = summarizeCart([
(book: dune, quantity: 2),
(book: middlemarch, quantity: 1),
]);
expect(summary, (itemCount: 3, total: 125300));
expect(formatTry(summary.total), '1253.00 TRY');
});
test('const books with the same values are the same instance', () {
const again = Book(
isbn: '9780441172719',
title: 'Dune',
author: herbert,
price: 48900,
publishedYear: 1965,
);
expect(identical(dune, again), isTrue);
});
flutter test
00:00 +0: test/domain/models/book_test.dart: displayTitle adds the subtitle only when there is one
00:00 +1: test/domain/models/book_test.dart: summarizeCart counts items and sums prices in kurus
00:00 +2: test/domain/models/book_test.dart: const books with the same values are the same instance
00:00 +3: test/widget_test.dart: tapping the button adds a book to the cart
00:00 +4: All tests passed!
Record eşitliği testi sadeleştiriyor: expect(summary, (itemCount: 3, total: 125300)) iki alanı tek satırda karşılaştırıyor.
Üretimde Dikkat
Para için double kullanmama kuralını API sınırında da uygula. Bookstore API'si fiyatı JSON'da 489.00 gibi bir sayı olarak gönderir; Dart'ın jsonDecode'u bunu double olarak okur. Kuruşa çevirmeyi tek bir yerde, JSON'dan model oluşturulan noktada yap ve yuvarlamayı açıkça seç. API entegrasyonu yazısında bunu gerçek yanıtla göreceğiz.
! ve late kullanımını code review'da işaretle. İkisi de derleyicinin yaptığı kontrolü çalışma anına taşıyor. Bir dosyada çok sayıda ! görüyorsan genellikle bir tip nullable olmaması gereken yerde nullable tanımlanmıştır.
Web'e de build alacaksan int'in 53 bit sınırını hatırla. ISBN (kitapların uluslararası kimlik numarası) gibi 13 haneli bir değeri int tutmak mobilde sorunsuz çalışır, web'de de bu aralıkta kalır; ama veritabanından gelen 64 bitlik bir ID'yi int olarak taşımak web'de sessizce yanlış değer üretebilir. Kimlikleri string tutmak iki platformda da güvenli.
Sık Düşülen Tuzaklar
Birinci tuzak, final list'i immutable sanmak. final cart = ['Dune']; yazdığında cart'a başka bir list atayamazsın ama içine eleman ekleyebilirsin. Gerçekten değişmeyen bir collection istiyorsan const ya da List.unmodifiable kullan.
İkinci tuzak, C# alışkanlığıyla !'i "uyarıyı sustur" anlamında kullanmak. Dart'ta ! bir çalışma anı kontrolüdür; değer null ise uygulama o satırda hata fırlatır. Flutter'da bu, kullanıcının ekranında kırmızı bir hata ekranı (debug) ya da gri bir alan (release) demek.
Üçüncü tuzak, named parametrelerin varsayılan olarak opsiyonel olduğunu unutmak. {int year} yazarsan derleyici, non-nullable bir parametrenin varsayılan değeri olmadığı için hata verir; çözüm ya required ya da bir varsayılan değer. Nullable yapmak (int? year) çoğu zaman yanlış çözümdür.
Dördüncü tuzak, record'u her yerde class yerine kullanmak. Record'lar bir function'dan birden fazla değer döndürmek ya da kısa ömürlü veri için iyi; domain modeli için ad, davranış ve doğrulama gerekiyor, yani class.
Küçük Bir Deneme
★☆☆ variables.dart'a const featured yerine final featured = List.unmodifiable(['Dune', 'Emma']) yaz. Çıktı değişiyor mu? identical ile iki List.unmodifiable sonucunu karşılaştır.
★★☆ summarizeCart'a ücretsiz kargo bilgisini ekle: dönüş tipi ({int itemCount, int total, bool freeShipping}) olsun ve 1.000,00 TL üstünde true dönsün. Sınırı digit separator ile yaz ve bir test ekle.
★★★ Book'a List<String> tags ekle ve const constructor'ı koru. Testteki dune sabitinde tags: ['classic'] verdiğinde ne oluyor, tags: const ['classic'] verdiğinde ne oluyor? İkisinden birinde identical(dune, again) testi hâlâ geçiyor mu?
Bir sonraki yazıda Dart'ın ikinci yarısına geçiyoruz: constructor çeşitleri ve Dart 3.13'ün primary constructor'ları, her class'ın aynı zamanda bir interface olması, mixin'ler, extension type'lar ve sipariş durumunu modelleyen bir sealed class ile pattern matching.