Dart'ta Kalıtım (Inheritance) ve Polymorphism Rehberi
Son güncelleme:
6 dk okuma

Kalıtım, nesne yönelimli programlamayı yeni öğrenenlerin en hızlı sevdiği ve en çabuk aşırı kullandığı araçtır. Derslerde sık gördüğüm tablo şu: iki sınıfta ortak iki alan var diye biri diğerinden türetilir, birkaç hafta sonra da beş katmanlı, değiştirilmesi korkutucu bir hiyerarşi ortaya çıkar. Bu yazıda kalıtımı doğru yerde kullanmak için gereken her şeyi ele alıyoruz: extends, super, @override, polymorphism, abstract sınıflar, güvenli tip kontrolü ve Dart 3 ile gelen sealed, final, base gibi sınıf niteleyicileri.
Bir önceki yazıda enum ve composition ile "sahiptir" ilişkisini kurmuştuk. Kalıtım ise "bir türüdür" ilişkisini modeller.
Canlı Demo: Inheritance ve Polymorphism
Kalıtım ve polymorphism kavramlarını interaktif olarak deneyin:
Kalıtım (Inheritance) Nedir?
Kalıtımda bir sınıf, başka bir sınıfın alanlarını ve metotlarını devralır. Devreden sınıfa üst sınıf (superclass), devralan sınıfa alt sınıf (subclass) denir. Türkçe derslerde sıkça "baba ve çocuk sınıf" benzetmesi de kullanılır.
İlişki tek yönlüdür: alt sınıf üst sınıfın her şeyine sahiptir, üst sınıf ise alt sınıfın eklediklerinden habersizdir. Dart'ta bir sınıf yalnızca tek bir sınıftan extends edebilir; davranışı birden fazla kaynaktan paylaşmak için mixin'ler vardır, onları interface ve implements yazısında ele alıyoruz.
İlk Örnek: Ev, Saray ve Villa
class Ev {
final int pencereSayisi;
const Ev(this.pencereSayisi);
String tanit() => '$pencereSayisi pencereli bir ev';
}
class Saray extends Ev {
final int kuleSayisi;
const Saray(super.pencereSayisi, {required this.kuleSayisi});
@override
String tanit() => '${super.tanit()}, $kuleSayisi kulesi var';
}
class Villa extends Ev {
final bool garajVarMi;
const Villa(super.pencereSayisi, {this.garajVarMi = false});
}
void main() {
const ev = Ev(4);
const saray = Saray(50, kuleSayisi: 3);
const villa = Villa(12, garajVarMi: true);
print(ev.tanit()); // 4 pencereli bir ev
print(saray.tanit()); // 50 pencereli bir ev, 3 kulesi var
print(villa.tanit()); // 12 pencereli bir ev
print(villa.garajVarMi); // true
}Saray ve Villa, pencereSayisi alanını ve tanit metodunu Ev'den alır; her biri yalnızca kendi farkını (kule sayısı, garaj) ekler. Ortak olan yukarıda, değişen aşağıdadır.
super ile Üst Sınıfa Ulaşmak
super kelimesi iki yerde karşınıza çıkar:
- Constructor'da:
Saray(super.pencereSayisi, ...)yazımı, değeri doğrudan üst sınıfın constructor'ına iletir. Bu, Dart 2.17 ile gelen "super parametreleri" özelliğidir ve eski örneklerde gördüğünüz: super(pencereSayisi)yazımının kısa hâlidir. - Metot içinde:
super.tanit()üst sınıfın metodunu çağırır.Saray.tanitüst sınıfın cümlesini baştan yazmak yerine onu kullanıp sonuna ekleme yapıyor. Davranışı tamamen değiştirmek yerine genişletmek istediğinizde bu kalıbı kullanırsınız.
@override: Alt Sınıfın Kendi Davranışı
Alt sınıf, üst sınıftan gelen bir metodu kendi gövdesiyle değiştirebilir. Buna override (ezme) denir:
class Hayvan {
void sesCikar() {
print('Ses yok');
}
}
class Memeli extends Hayvan {}
class Kedi extends Memeli {
@override
void sesCikar() => print('Miyav');
}
class Kopek extends Memeli {
@override
void sesCikar() => print('Hav hav');
}
class Tilki extends Memeli {
// sesCikar yazılmadı: Hayvan'daki gövde kullanılır
}
void main() {
Kedi().sesCikar(); // Miyav
Kopek().sesCikar(); // Hav hav
Tilki().sesCikar(); // Ses yok
Hayvan().sesCikar(); // Ses yok
}Hangi gövdenin çalışacağına nesnenin gerçek sınıfından başlayarak karar verilir: Tilki'de sesCikar yoksa Memeli'ye, orada da yoksa Hayvan'a bakılır. Metodun hiç var olup olmadığı ise derleme zamanında kontrol edilir; zincirin hiçbir yerinde olmayan bir metodu çağırmaya çalışırsanız kod derlenmez.
@override anotasyonu zorunlu değildir, ama her zaman yazın. Üst sınıfta o isimde bir metot yoksa (örneğin büyük harfi unutup sescikar yazdıysanız) analyzer sizi uyarır. Anotasyon olmasa bu hata sessizce yeni bir metot tanımlar.
Abstract Sınıflar: "Ses Yok" Yerine Sözleşme
Yukarıdaki örnekte bir tasarım sorunu var: Tilki için ses yazmayı unuttuk ve program bunu "Ses yok" diye geçiştirdi. Üst sınıfa anlamsız bir varsayılan gövde koymak, unutulan override'ları gizler. Doğrusu, Hayvan'ı abstract yapıp metodu gövdesiz bırakmaktır:
abstract class Hayvan {
String get ad;
String sesCikar(); // Gövde yok: her alt sınıf yazmak zorunda
void tanit() => print('$ad: ${sesCikar()}');
}
class Kedi extends Hayvan {
@override
String get ad => 'Kedi';
@override
String sesCikar() => 'Miyav';
}
class Tilki extends Hayvan {
@override
String get ad => 'Tilki';
@override
String sesCikar() => 'Yip yip';
// Bu metodu yazmasaydık kod derlenmezdi.
}
void main() {
// Hayvan(); // Hata: abstract sınıftan nesne oluşturulamaz
Kedi().tanit(); // Kedi: Miyav
Tilki().tanit(); // Tilki: Yip yip
}Abstract sınıf hem sözleşme (gövdesiz sesCikar) hem de ortak davranış (tanit) taşıyabilir. Bu karışım, onu yalnızca sözleşme içeren interface'lerden ayırır.
Polymorphism: Aynı Çağrı, Farklı Davranış
Polymorphism, değişkenin tipi üst sınıf olsa bile çağrının nesnenin gerçek sınıfındaki gövdeyi çalıştırmasıdır.
Hayvan h = Kedi();
print(h.sesCikar()); // Miyavh değişkeninin tipi Hayvan, ama tuttuğu nesne bir Kedi. Derleyici h üzerinden yalnızca Hayvan'da tanımlı üyeleri çağırmanıza izin verir; çalışma zamanında ise Kedi'nin gövdesi çalışır.
Bunun asıl gücü, farklı alt sınıfları tek bir tip üzerinden yönetebilmektir. Bir List içinde ya da bir fonksiyon parametresinde:
void hepsiniKonustur(List<Hayvan> hayvanlar) {
for (final hayvan in hayvanlar) {
hayvan.tanit();
}
}
void main() {
hepsiniKonustur([Kedi(), Tilki()]);
// Kedi: Miyav
// Tilki: Yip yip
}hepsiniKonustur fonksiyonu hangi hayvanların var olduğunu bilmez. Yarın Kopek sınıfını eklediğinizde bu fonksiyona tek satır dokunmanız gerekmez. İyi tasarlanmış polymorphism'in işareti budur: yeni bir tür eklemek, mevcut kodu değiştirmeyi gerektirmez.
Tip Kontrolü: is, as ve Pattern'ler
Alt sınıf nesnesi her zaman üst sınıf tipine atanabilir (upcasting); bu otomatiktir ve güvenlidir. Ters yön (downcasting) ise risklidir:
void main() {
Ev ev = const Saray(50, kuleSayisi: 3); // Upcasting: otomatik
// Villa villa = ev as Villa; // TypeError: bu nesne bir Villa değil
if (ev is Saray) {
// Blok içinde ev otomatik olarak Saray tipine daraldı
print('Kule sayısı: ${ev.kuleSayisi}');
}
}Bu yazının eski sürümünde Ev(10) as Villa örneği "zorla çevirme" olarak gösteriliyordu. Bu kod derlenir ama çalışma zamanında TypeError fırlatır: as nesneyi başka bir sınıfa dönüştürmez, yalnızca "bu zaten bir Villa" iddiasında bulunur. as'ın genel mantığını tip dönüşümleri yazısında ele almıştık. Kural basit: önce is ile kontrol edin, kontrol ettiyseniz as yazmanıza gerek kalmaz.
Dart 3'teki pattern'ler aynı kontrolü alanlarla birlikte yapabilir:
String evOzeti(Ev ev) => switch (ev) {
Saray(:final kuleSayisi) => 'Saray, $kuleSayisi kule',
Villa(garajVarMi: true) => 'Garajlı villa',
Villa() => 'Garajsız villa',
_ => 'Sıradan ev',
};
void main() {
print(evOzeti(const Villa(12, garajVarMi: true))); // Garajlı villa
print(evOzeti(const Ev(4))); // Sıradan ev
}Saray(:final kuleSayisi) hem tipi kontrol eder hem de kuleSayisi alanını aynı isimli bir değişkene çıkarır. Villa(garajVarMi: true) ise yalnızca garajı olan villalarla eşleşir. Case'ler yukarıdan aşağı denendiği için özel durum genel durumdan önce yazılır.
Dart 3 Sınıf Niteleyicileri
Dart 3 ile bir sınıfın nasıl genişletilebileceğini ya da uygulanabileceğini kontrol eden niteleyiciler geldi. Önemli bir ayrıntı: bu kısıtlamalar sınıfın tanımlandığı kütüphanenin (genellikle aynı dosyanın) dışında geçerlidir. Aynı dosya içinde sınıfı yine genişletebilirsiniz.
sealed: Kapalı Hiyerarşi ve Eksiksiz switch
Günlük uygulama kodunda en çok işinize yarayacak olan sealed'dır. Enum yazısında ekran durumlarını enum ile modellemiştik ve bir sınıra takılmıştık: enum değerleri kendi verisini taşıyamaz. Sealed sınıflar bu sınırı kaldırır:
sealed class YuklemeDurumu {}
class Yukleniyor extends YuklemeDurumu {}
class Basarili extends YuklemeDurumu {
final List<String> veriler;
Basarili(this.veriler);
}
class Hata extends YuklemeDurumu {
final String mesaj;
Hata(this.mesaj);
}
String ozet(YuklemeDurumu durum) => switch (durum) {
Yukleniyor() => 'Yükleniyor...',
Basarili(:final veriler) => '${veriler.length} kayıt geldi',
Hata(:final mesaj) => 'Hata: $mesaj',
};
void main() {
print(ozet(Basarili(['a', 'b']))); // 2 kayıt geldi
print(ozet(Hata('Bağlantı yok'))); // Hata: Bağlantı yok
}Sealed bir sınıf örtük olarak abstract'tır, doğrudan nesnesi oluşturulamaz. Doğrudan alt sınıfları aynı kütüphanede tanımlanmak zorundadır, bu yüzden derleyici tüm alt türleri bilir. Sonuç: switch'te Hata durumunu unutursanız kod derlenmez, tıpkı enum'da olduğu gibi. Üstelik her durum kendi verisini (veriler, mesaj) taşır.
Diğer Niteleyiciler
abstract: Nesnesi oluşturulamaz, gövdesiz metot içerebilir. Her yerden genişletilebilir ve uygulanabilir.base: Kütüphane dışından yalnızcaextendsile genişletilebilir,implementsile uygulanamaz. Böylece üst sınıfın constructor'ı ve özel üyeleri her alt türde garanti altında olur.interface: Kütüphane dışından yalnızcaimplementsile uygulanabilir,extendsile genişletilemez. Ayrıntıları interface yazısında.final: Kütüphane dışından ne genişletilebilir ne uygulanabilir. Hiyerarşiyi tamamen kapatır.sealed:finalgibi dışarıya kapalıdır, ek olarak alt türleri üzerinde eksiksizlik kontrolü sağlar.
base ya da final bir sınıfın aynı kütüphanedeki alt türleri de base, final veya sealed olarak işaretlenmek zorundadır; böylece kısıtlama hiyerarşinin aşağısında delinemez.
Tek bir uygulama içinde çalışıyorsanız sealed dışındaki niteleyicilere nadiren ihtiyaç duyarsınız. Asıl değerleri paket ya da ekipler arası paylaşılan modül yazarken ortaya çıkar: bir sınıfın dışarıdan nasıl kullanılacağını açıkça belirler ve ileride o sınıfı değiştirdiğinizde kimin kodunun bozulabileceğini sınırlar.
Flutter'da Kalıtım
Flutter'da her widget kalıtımla başlar:
import 'package:flutter/material.dart';
class Karsilama extends StatelessWidget {
const Karsilama({super.key, required this.isim});
final String isim;
@override
Widget build(BuildContext context) {
return Text('Merhaba $isim');
}
}StatelessWidget üst sınıftır, Karsilama alt sınıf. super.key yukarıda gördüğümüz super parametresidir, build metodu da bir override'dır. Kendi widget'ınızı oluştururken bu kalıbı her seferinde kurarsınız.
Dikkat edilecek nokta: Flutter kalıtımı yalnızca bu sözleşme için kullanır. Text ya da Container'ı genişletip özelleştirmezsiniz; onları build içinde birleştirirsiniz. Yani widget'ların kendisi kalıtımla, içerikleri composition ile kurulur.
Sık Yapılan Hatalar
- Yalnızca birkaç ortak alan için kalıtım kullanmak; ilişki "bir türüdür" diye okunmuyorsa composition seçin.
- Derin hiyerarşiler kurmak; iki üç seviyeyi geçen zincirler değişikliği zorlaştırır.
- Üst sınıfa "Ses yok" gibi anlamsız varsayılan gövdeler koymak; abstract metot unutulan override'ı derleme zamanında yakalar.
iskontrolü yapmadanasile downcasting yapmak.@overrideyazmamak; yazım hatası sessizce yeni bir metot yaratır.- Durum modellerinde
sealedyerinedefaultdallı switch'lere güvenmek.
Sonraki Adım
Kalıtımın "tek üst sınıf" sınırını aşmak ve sınıfları yalnızca sözleşmeyle bağlamak için sıradaki konu interface ve implements. Orada abstract interface class ve mixin'leri de ele alıyoruz.
Kaynaklar
Soru ve önerileriniz için:
Sık Sorulan Sorular
extends ile implements arasındaki fark nedir?
extends üst sınıfın hem alanlarını hem metot gövdelerini devralır ve yalnızca tek bir sınıftan yapılabilir. implements ise sadece sözleşmeyi alır; tüm üyeleri kendiniz yazarsınız ve birden fazla arayüz uygulayabilirsiniz.
@override yazmayı unutursam ne olur?
Kod yine derlenir ve metot ezilir; anotasyon zorunlu değildir. Ancak analyzer uyarı verir ve üst sınıfta o isimde bir metot yoksa yazım hatasını yakalamanızı sağlar, bu yüzden her zaman ekleyin.
Downcasting yaparken hatadan nasıl korunurum?
as kullanmadan önce is ile kontrol edin. Dart, if (ev is Villa) bloğunun içinde yerel değişkeni otomatik olarak Villa tipine daraltır (type promotion), böylece as yazmaya gerek bile kalmaz. Birden fazla alt tür söz konusuysa switch ve pattern'ler daha okunaklıdır.
sealed class ne işe yarar?
sealed bir sınıfın tüm doğrudan alt türleri aynı kütüphanede tanımlanır, bu yüzden derleyici hepsini bilir. Böyle bir tip üzerindeki switch eksiksiz olmak zorundadır; bir alt türü unutursanız kod derlenmez. Kendi verisini taşıyan durumları (yükleniyor, başarılı, hata) modellemek için idealdir.
İlgili Yazılar
Dart'ta Interface ve Implements: Sınıflara Ekstra Özellik
Dart'ta implements ile sözleşme kurmak, Dart 3'ün interface class ve abstract interface class yapıları, mixin ile with farkı ve test edilebilir kod örneği.
Dart'ta Enum ve Composition: Kodu Nasıl Düzenleriz?
Dart 3'te alan ve metot taşıyan enum'lar, switch ifadeleriyle eksiksiz kontrol ve composition ile has-a ilişkisi; örnekler ve sık yapılan hatalarla.
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.