Dart Collections: List, Set ve Map (HashSet, HashMap)
Son güncelleme:
7 dk okuma

Koleksiyonlar Flutter kodunun her yerindedir: ekrandaki ürün listesi bir List, seçili etiketler bir Set, sunucudan gelen JSON bir Map. Öğrencilerin bu konuda yaptığı en yaygın hata, her şey için List kullanmak ve sonra "bu eleman listede var mı?" sorusunu her seferinde baştan sona dolaşarak cevaplamaktır. Doğru koleksiyonu seçmek, çoğu zaman doğru algoritmayı seçmekle aynı şeydir.
Bu yazıda üç temel koleksiyonu, dart:collection içindeki HashSet ve HashMap ile varsayılan Set ve Map arasındaki farkı, kendi sınıflarınızı koleksiyonlarda doğru kullanmanın kuralını ve Dart 3 ile gelen kullanışlı yazımları ele alıyoruz. Bir önceki konu interface ve implements idi; burada tasarladığımız nesneleri bir arada tutmayı öğreniyoruz.
Canlı Demo: Dart Collections Örnekleri
List, HashSet ve HashMap kullanımlarını interaktif olarak deneyin:
List: Sıralı ve Index ile Erişilen
List, diğer dillerdeki dizilere (array) benzer. Elemanlar eklendiği sırada durur, her birinin 0'dan başlayan bir index'i vardır ve aynı değer birden fazla kez bulunabilir.
void main() {
final sayilar = [3, 4, 5]; // List<int>
final isimler = <String>['Ahmet', 'Mehmet', 'Selda'];
final oranlar = [3.2, 3.3, 3.5]; // List<double>
print(sayilar); // [3, 4, 5]
print(isimler); // [Ahmet, Mehmet, Selda]
print(oranlar); // [3.2, 3.3, 3.5]
}Tipi her seferinde List<int> diye yazmanız gerekmez; Dart literal'den çıkarır. Boş bir liste oluştururken ise tipi belirtin (<String>[]), aksi hâlde List<dynamic> elde edersiniz.
Ekleme, Güncelleme ve Silme
void main() {
final sayilar = [3, 4, 5];
sayilar.add(32); // Sona ekler: [3, 4, 5, 32]
sayilar.insert(0, 100); // Başa ekler: [100, 3, 4, 5, 32]
sayilar.addAll([7, 8]); // Birden fazla ekler
sayilar[0] = 50; // Güncelleme
sayilar.removeAt(0); // Index'e göre siler
sayilar.remove(32); // Değere göre siler (ilk eşleşeni)
sayilar.removeWhere((s) => s > 6); // Koşula göre siler
print(sayilar); // [3, 4, 5]
}Sık Kullanılan Özellikler
void main() {
final sayilar = [10, 20, 30, 40];
print(sayilar.isEmpty); // false
print(sayilar.isNotEmpty); // true
print(sayilar.length); // 4
print(sayilar.first); // 10
print(sayilar.last); // 40
print(sayilar.contains(20)); // true
print(sayilar.indexOf(30)); // 2
for (final sayi in sayilar) {
print('Sayı: $sayi');
}
}first ve last boş listede StateError fırlatır. Listenin boş olabileceği yerlerde Dart 3.0 ile gelen firstOrNull ve lastOrNull daha güvenlidir.
Koleksiyon if, for ve spread
Dart, liste literal'inin içinde koşul ve döngü yazmanıza izin verir. Flutter'da widget listeleri kurarken sürekli kullanılır:
void main() {
const girisYapti = true;
final temel = ['Ana Sayfa', 'Ara'];
final menu = [
...temel, // spread: başka listeyi açar
if (girisYapti) 'Profil', // koşullu eleman
for (var i = 1; i <= 2; i++) 'Sayfa $i',
];
print(menu); // [Ana Sayfa, Ara, Profil, Sayfa 1, Sayfa 2]
}Nesne Listesi: Sıralama ve Filtreleme
Gerçek uygulamalarda listeler çoğunlukla kendi sınıflarınızın nesnelerini tutar:
class Ogrenci {
final int no;
final String ad;
final String sinif;
const Ogrenci(this.no, this.ad, this.sinif);
@override
String toString() => '$no - $ad ($sinif)';
}
void main() {
final ogrenciler = [
const Ogrenci(33, 'Ahmet', '3. Sınıf'),
const Ogrenci(199, 'Ayşe', '4. Sınıf'),
const Ogrenci(22, 'Mehmet', '2. Sınıf'),
];
for (final ogrenci in ogrenciler) {
print(ogrenci);
}
}Bu yazının eski sürümünde alanlar int? no şeklinde nullable tanımlanmıştı ve her kullanımda no! yazılıyordu. Bir öğrencinin numarası hiçbir zaman boş olmayacaksa alanı nullable yapmak yalnızca ! operatörünü ve çalışma zamanı hatası riskini koda taşır. Değer her zaman varsa final int no yazın.
toString override'ı da küçük ama faydalı bir alışkanlıktır: print(ogrenci) artık Instance of 'Ogrenci' yerine okunabilir bir satır basar.
Sıralama
ogrenciler.sort((a, b) => a.no.compareTo(b.no));
print(ogrenciler); // [22 - Mehmet (2. Sınıf), 33 - Ahmet (3. Sınıf), 199 - Ayşe (4. Sınıf)]
ogrenciler.sort((a, b) => a.ad.compareTo(b.ad)); // İsme göresort listeyi yerinde değiştirir ve geriye bir şey döndürmez. final sirali = ogrenciler.sort(...) yazmak derslerde çok sık gördüğüm bir hatadır; derleyici void bir değerin kullanılamayacağını söyler. Orijinal listeye dokunmadan sıralı bir kopya istiyorsanız önce kopyalayın: final sirali = [...ogrenciler]..sort(...).
compareTo metin karşılaştırmasını karakter kodlarına göre yapar; Türkçe'deki "Ç", "Ş", "Ü" gibi harfler alfabetik sıraya tam oturmayabilir. Kullanıcıya gösterilen isim listelerinde yerel sıralama gerekiyorsa bunu ayrıca ele almak gerekir.
Filtreleme ve Dönüştürme
void main() {
final ogrenciler = [
const Ogrenci(33, 'Ahmet', '3. Sınıf'),
const Ogrenci(199, 'Ayşe', '4. Sınıf'),
const Ogrenci(22, 'Mehmet', '2. Sınıf'),
];
final kucukNumaralar = ogrenciler.where((o) => o.no < 100).toList();
final isimler = ogrenciler.map((o) => o.ad).toList();
final ayseVarMi = ogrenciler.any((o) => o.ad == 'Ayşe');
final hepsiPozitif = ogrenciler.every((o) => o.no > 0);
final bulunan = ogrenciler.where((o) => o.no == 22).firstOrNull;
print(kucukNumaralar); // [33 - Ahmet (3. Sınıf), 22 - Mehmet (2. Sınıf)]
print(isimler); // [Ahmet, Ayşe, Mehmet]
print('$ayseVarMi $hepsiPozitif'); // true true
print(bulunan?.ad); // Mehmet
}where ve map tembel (lazy) bir Iterable döndürür; elemanlar ancak dolaşıldığında hesaplanır. Index ile erişmek ya da sonucu birden fazla kez kullanmak istiyorsanız .toList() ile somut bir listeye çevirin. Flutter'da arama kutusu gibi özellikler tam olarak bu where kalıbıyla yazılır.
Set: Tekrarsız Elemanlar
Set, aynı elemanı yalnızca bir kez tutar ve "bu eleman var mı?" sorusunu listeden çok daha hızlı cevaplar. List'te contains tüm elemanlara tek tek bakar; Set'te ise bu sorgu ortalama olarak sabit sürede tamamlanır. Neden böyle olduğunu Big O yazısında ele almıştım.
void main() {
final etiketler = {'dart', 'flutter'}; // Set<String>
final eklendi = etiketler.add('mobil'); // true
final tekrar = etiketler.add('dart'); // false: zaten var
print(etiketler); // {dart, flutter, mobil}
print('$eklendi $tekrar'); // true false
final benzersiz = [1, 2, 3, 4, 1].toSet();
print(benzersiz); // {1, 2, 3, 4}
final a = {1, 2, 3};
final b = {2, 3, 4};
print(a.union(b)); // {1, 2, 3, 4}
print(a.intersection(b)); // {2, 3}
print(a.difference(b)); // {1}
}Dikkat edilecek bir tuzak: {} yazdığınızda boş bir Set değil, boş bir Map oluşur. Boş Set için tipi belirtin: <String>{}.
Set ile HashSet Arasındaki Fark
Bu yazının başlığındaki HashSet, dart:collection kütüphanesinden gelir. {...} literal'i ya da Set() ise varsayılan olarak bir LinkedHashSet oluşturur. Farkı şudur:
Setliteral'i (LinkedHashSet): Elemanları ekleme sırasıyla dolaştırır.HashSet: Sıra garantisi vermez; dolaşma sırası ekleme sırasından farklı olabilir.
import 'dart:collection';
void main() {
final sirali = {5, 3, 8}; // Her zaman 5, 3, 8 sırasıyla
final sirasiz = HashSet<int>.from([5, 3, 8]); // Sıra garanti değil
print(sirali);
print(sirasiz);
}Günlük kodda Set literal'i neredeyse her zaman yeterlidir ve öngörülebilir sırası hata ayıklamayı kolaylaştırır. HashSet'i sıranın gerçekten önemsiz olduğu ve bunu kodda açıkça belirtmek istediğiniz durumlarda seçin.
Kendi Sınıfınızı Set'te Kullanmak: == ve hashCode
Set, iki nesnenin "aynı" olup olmadığına == operatörü ve hashCode ile karar verir. Varsayılan olarak iki ayrı nesne, alanları aynı olsa bile farklı sayılır. Aynı numaralı öğrencilerin aynı kabul edilmesini istiyorsanız ikisini birlikte override edersiniz:
class Ogrenci {
final int no;
final String ad;
const Ogrenci(this.no, this.ad);
@override
bool operator ==(Object other) => other is Ogrenci && other.no == no;
@override
int get hashCode => no.hashCode;
}
void main() {
final ogrenciler = <Ogrenci>{};
ogrenciler.add(const Ogrenci(123, 'Ahmet'));
ogrenciler.add(const Ogrenci(124, 'Mehmet'));
ogrenciler.add(const Ogrenci(123, 'Farklı İsim')); // Aynı numara: eklenmez
print('Toplam öğrenci: ${ogrenciler.length}'); // 2
}Eski sürümdeki == metodu (other as Ogrenci).no şeklinde yazılmıştı. Bu, Ogrenci olmayan bir nesneyle karşılaştırmada TypeError fırlatır; == ise hiçbir zaman hata fırlatmamalı, farklı tipte bir nesne için sadece false döndürmelidir. other is Ogrenci && ... kalıbı bunu sağlar.
Kural nettir: == iki nesneye eşit diyorsa hashCode değerleri de eşit olmak zorundadır. Birden fazla alan karşılaştırılıyorsa Object.hash(no, ad) kullanılabilir. Bu eşitlik yalnızca değişmeyen (final) alanlara dayanmalıdır; Set'e eklendikten sonra hash'i değişen bir nesne artık bulunamaz.
Map: Anahtardan Değere
Map, her değeri benzersiz bir anahtara (key) bağlar. Bir apartman düşünün: her dairenin bir numarası (anahtar) ve içinde oturan biri (değer) vardır.
void main() {
final stok = {'elma': 12, 'armut': 0}; // Map<String, int>
stok['muz'] = 30; // Ekleme
stok['elma'] = 10; // Güncelleme
stok.update('armut', (v) => v + 5); // Var olan değeri değiştirme
stok.putIfAbsent('kivi', () => 8); // Yoksa ekleme
print(stok['elma']); // 10
print(stok['ananas']); // null: anahtar yok
print(stok['ananas'] ?? 0); // 0
print(stok.containsKey('muz')); // true
stok.remove('muz');
print(stok); // {elma: 10, armut: 5, kivi: 8}
}Map'ten okunan değer her zaman nullable'dır (int?), çünkü anahtar bulunmayabilir. ! ile zorlamak yerine ?? ile varsayılan değer vermek ya da önce containsKey ile kontrol etmek daha güvenlidir.
Kimlik Numarasıyla Kişi Bulma
class Vatandas {
final String tc;
final String isim;
final String soyisim;
const Vatandas(this.tc, this.isim, this.soyisim);
}
void main() {
const kisiler = [
Vatandas('12345678901', 'Ahmet', 'Balaman'),
Vatandas('98765432109', 'Mehmet', 'Yılmaz'),
Vatandas('55555555555', 'Ayşe', 'Demir'),
];
// Anahtar TC, değer kişinin kendisi
final vatandaslar = {for (final k in kisiler) k.tc: k};
final bulunan = vatandaslar['12345678901'];
print('${bulunan?.isim} ${bulunan?.soyisim}'); // Ahmet Balaman
}Eski sürümde TC numarası int olarak tutuluyordu. Kimlik, telefon ya da posta kodu gibi sayıya benzeyen ama üzerinde hesap yapılmayan değerleri String tutun: başındaki sıfırlar korunur, uzunluk sınırına takılmazsınız ve kimse yanlışlıkla iki TC numarasını toplamaz. Map'i kurarken kullanılan {for (...) anahtar: değer} yazımı da koleksiyon for'un Map'teki karşılığıdır.
Map'i Dolaşmak
for (final MapEntry(key: tc, value: kisi) in vatandaslar.entries) {
print('TC: $tc');
print('İsim: ${kisi.isim} ${kisi.soyisim}');
}Dart 3'teki pattern'ler sayesinde her MapEntry'yi döngü başında anahtar ve değer olarak ayırabilirsiniz. Bu yöntem, anahtarları dolaşıp her birinde vatandaslar[tc] ile nullable bir değer okumaktan daha temizdir. Yalnızca değerler gerekiyorsa vatandaslar.values, yalnızca anahtarlar gerekiyorsa vatandaslar.keys kullanılır.
Map ile HashMap
Set'teki ayrım burada da geçerlidir. {...} literal'i bir LinkedHashMap oluşturur ve anahtarları ekleme sırasıyla dolaştırır; dart:collection içindeki HashMap ise sıra garantisi vermez. Günlük kodda literal yeterlidir.
List, Set ve Map Karşılaştırması
| Özellik | List | Set | Map |
|---|---|---|---|
| Sıra | Ekleme sırası | Literal'de ekleme sırası, HashSet'te garanti yok |
Literal'de ekleme sırası, HashMap'te garanti yok |
| Tekrar eden değer | Olabilir | Olamaz | Anahtar olamaz, değer olabilir |
| Erişim yolu | Index | Yalnızca "var mı?" sorusu ve dolaşma | Anahtar |
| "İçinde var mı?" maliyeti | Tüm elemanlara bakar | Ortalama sabit süre | Anahtar için ortalama sabit süre |
Seçim, çoğu zaman hızdan çok "veriyi neye göre bulacağım?" sorusuna bağlıdır:
- List: Sıranın önemli olduğu her şey. Alışveriş sepeti, mesajlar, ekranda gösterilen sonuçlar.
- Set: Tekrarın anlamsız olduğu şeyler. Seçili etiketler, favori ID'leri, daha önce görülen kayıtlar.
- Map: Bir anahtarla aranan her şey. Ayarlar, ID'ye göre önbellek, JSON verisi.
Flutter'da Koleksiyonlar
Flutter'da en çok List kullanılır. Bir ListView listedeki her eleman için bir satır üretir:
import 'package:flutter/material.dart';
class UrunListesi extends StatelessWidget {
const UrunListesi({super.key});
static const urunler = ['Elma', 'Armut', 'Muz'];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: urunler.length,
itemBuilder: (context, index) => ListTile(title: Text(urunler[index])),
);
}
}Seçim durumunu tutmak için Set biçilmiş kaftandır. Bir öğenin seçili olup olmadığını sormak ve seçimi tersine çevirmek tek satırdır:
final secililer = <String>{};
void secimiDegistir(String urun) {
if (!secililer.remove(urun)) {
secililer.add(urun); // remove false döndüyse eleman yoktu: ekle
}
}jsonDecode sonucu ise Map<String, dynamic> olarak gelir; alanları okurken tip dönüşümleri yazısındaki kurallar geçerlidir.
Sık Yapılan Hatalar
- Her şey için List kullanıp
containsile sürekli arama yapmak; tekrarsız ve sık aranan veri için Set seçin. - Boş Set için
{}yazmak; bu bir Map oluşturur,<T>{}kullanın. sortsonucunu bir değişkene atamaya çalışmak;sortlisteyi yerinde değiştirir vevoiddöndürür.==metodundaaskullanmak; farklı tipte nesneyle karşılaştırmada hata fırlatır.==override ediphashCode'u unutmak; Set ve Map nesneyi bulamaz.- Map'ten okunan değere
!eklemek; anahtar yoksa uygulama çöker. - Boş olabilecek listede
firstkullanmak;firstOrNulltercih edin.
Sonraki Adım
Veriyle çalışırken işler her zaman yolunda gitmez: dosya bulunamaz, JSON beklenen biçimde gelmez, ağ bağlantısı kopar. Sıradaki konu try-catch ve async/await ile hata yönetimi.
Kaynaklar
İletişim:
Sık Sorulan Sorular
HashSet ile Set, HashMap ile Map arasında fark var mı?
Set ve Map soyut arayüzlerdir; {} literal'i varsayılan olarak ekleme sırasını koruyan LinkedHashSet ve LinkedHashMap üretir. dart:collection içindeki HashSet ve HashMap ise sıra garantisi vermez. Günlük Flutter kodunda çoğunlukla Set ve Map literal'leri yeterlidir.
Neden hashCode ve operator == birlikte override edilmeli?
Set ve Map bir elemanı ararken önce hashCode'a bakar, sonra == ile doğrular. Yalnızca birini ezerseniz aynı olması gereken nesneler farklı sayılabilir. Dart'ın kuralı nettir: == eşit diyorsa hashCode da eşit olmak zorundadır.
where ile filtreleyince neden List değil Iterable dönüyor?
where tembel (lazy) bir Iterable döndürür; elemanlar ancak dolaşıldığında hesaplanır. Index ile erişmek ya da sonucu ListView.builder'a vermek için .toList() çağırmanız gerekir.
Boş bir Set nasıl oluşturulur?
<String>{} ya da Set<String>() yazın. Tipsiz {} boş bir Map oluşturur; var s = {}; yazıp sonra s.add(...) çağırmaya çalışmak derleme hatası verir.
İlgili Yazılar
Flutter'da Kamerayı Kullanıcıya Bırakmak: Yön Zorlamadan Video Çekmek
Flutter camera paketinde ters dönen önizleme, kayıt başlarken donma ve kilitlenen kamera: Recapday'de çözdüğüm üç hatanın kodunu anlatıyorum.
En İyi Flutter Öğretmeni Seçimi: Mobil Uygulama Uzmanı Rehberi
Flutter için özel ders mi, kurs mu? Bir öğretmeni deneme dersinde nasıl değerlendirirsiniz, hangi sırayla öğrenirsiniz, özel ders ne zaman gereksiz?
Dart'ta Hata Yönetimi: Try-Catch ve Asenkron İşlemler
Dart'ta on ile hedefli try-catch, rethrow, kendi exception sınıfları, Future, async/await, zaman aşımı, paralel bekleme ve Flutter'da güvenli setState.