İçeriğe geç / Skip to content / Zum Inhalt

Dart Collections: List, Set ve Map (HashSet, HashMap)

Ahmet Balaman

Son güncelleme:

7 dk okuma

FlutterDartCollectionsListHashSetHashMapVeri Yapıları
Dart Collections: List, Set ve Map (HashSet, HashMap)

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öre

sort 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:

  • Set literal'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ı

List index ile erişilen sıralı kutular, HashSet tekrar kabul etmeyen sırasız bir küme, HashMap anahtardan değere giden ikililer

Ö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 contains ile 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.
  • sort sonucunu bir değişkene atamaya çalışmak; sort listeyi yerinde değiştirir ve void döndürür.
  • == metodunda as kullanmak; farklı tipte nesneyle karşılaştırmada hata fırlatır.
  • == override edip hashCode'u unutmak; Set ve Map nesneyi bulamaz.
  • Map'ten okunan değere ! eklemek; anahtar yoksa uygulama çöker.
  • Boş olabilecek listede first kullanmak; firstOrNull tercih 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.

Yorumlar