Ders 04 / 12
Trait'ler
Trait ile yöntemlerin sınıflar arasında paylaşılması, çakışmanın insteadof ve as ile çözülmesi, ve kullanılan trait'e aynı adlı yöntem eklenince kullananın durması.
Yayın Güncelleme
Bu derste
- Bir trait tanımlar ve use ile bir sınıfa katar
- İki trait aynı adlı yöntem getirdiğinde insteadof ve as ile çakışmayı çözer
- Kullanılan trait'e çakışan bir yöntem eklenmesinin kullananı durdurduğunu ölçer
- Sınıfın kendi yöntemi varsa aynı çakışmanın ulaşmadığını gösterir
İçindekiler
Gövdeli soyut sınıf paylaşım sağlıyordu ama sınıfa yalnız bir kalıtım hakkı bırakıyordu. Trait (trait) bu paylaşımı yatay yapar: bir yöntem kümesi bir kez yazılır ve birbiriyle akraba olmayan birçok sınıfa katılır. Soru, kullanılan bir trait’e yöntem eklenince kullanan sınıfın ne yaşadığıdır.
Trait ve use
Trait bir sınıf değildir ve örneği yaratılmaz. İçindeki yöntemler use ile bir sınıfın içine alınır ve sanki sınıfta yazılmış gibi çalışır. Bir sınıf istediği kadar trait kullanabilir; bu, kalıtımdaki tek üst sınıf kısıtını aşar.
<?php // Kayitli.php — kayit tutma davranisini tasiyan trait trait Kayitli { public function ozet(): string { return 'kayit'; } }
<?php // Zamanli.php — saat bilgisini tasiyan ikinci trait trait Zamanli { public function saat(): string { return '12:00'; } }
<?php // Belge.php — iki trait'i birlikte kullanan sinif require_once 'Kayitli.php'; require_once 'Zamanli.php'; class Belge { use Kayitli, Zamanli; }
<?php // belge_kullan.php — trait yontemleri sinifin kendi yontemi gibi cagrilir require 'Belge.php'; $belge = new Belge(); echo $belge->ozet(), ' ', $belge->saat(), "\n"; var_dump($belge instanceof Kayitli);
kayit 12:00 bool(false)
NM13. Belge ne ozet() ne de saat() yazdı ve ikisini de çağırabildik. instanceof Kayitli ise false verdi: trait bir tip değildir, bir sınıfın içine kopyalanan koddur. Bu yüzden trait’e bir parametre tipi olarak güvenilemez; tip gerekiyorsa arayüz dersindeki yola dönülür.
Çakışma Çözümü
İki trait aynı adda bir yöntem getirirse sınıf hangisini kullanacağını bildirmek zorundadır. insteadof seçileni belirler, as dışarıda kalan yönteme başka bir ad verir.
<?php // cakisma.php — iki trait ayni adli yontem getirir ve cakisma cozulur trait Kayitli { public function ozet(): string { return 'kayit'; } } trait Zamanli { public function saat(): string { return '12:00'; } public function ozet(): string { return 'zaman'; } } class Belge { use Kayitli, Zamanli { Kayitli::ozet insteadof Zamanli; Zamanli::ozet as zamanOzet; } } $belge = new Belge(); echo $belge->ozet(), ' ', $belge->zamanOzet(), ' ', $belge->saat(), "\n";
kayit zaman 12:00
NM14. ozet() adı Kayitli’ye gitti, Zamanli‘nin yöntemi zamanOzet() olarak kaldı. Çözüm blok kullanan sınıfın içindedir: çakışmayı çözmek kullananın işidir, trait’in değil. Aşağıdaki ölçüm bu cümlenin bedelini gösteriyor.
Trait’te Durum ve Soyut Yöntem
Trait yalnız yöntem taşımaz, özellik de taşır. Trait içindeki yöntemde $this, trait’i kullanan sınıfın nesnesidir; yani trait kendi başına çalışmaz, her zaman bir sınıfın içine girince anlam kazanır. Trait kullananından bir şey bekliyorsa bunu soyut yöntemle (abstract) bildirir: kullanan sınıf o yöntemi yazmak zorundadır. Her kullanan nesnenin özelliği kendine aittir, çünkü özellik sınıfın içine kopyalanmıştır.
<?php // Sayilabilir.php — ozellik ve soyut yontem tasiyan trait trait Sayilabilir { private int $sayac = 0; abstract public function ad(): string; public function artir(): static { $this->sayac++; return $this; } public function durum(): string { return $this->ad() . '=' . $this->sayac; } }
<?php // Ziyaret.php — trait'in bekledigi soyut yontemi yazan sinif require_once 'Sayilabilir.php'; class Ziyaret { use Sayilabilir; public function ad(): string { return 'ziyaret'; } }
<?php // ziyaret_kullan.php — iki nesne ayni trait'in ayri durumlarini tasir require 'Ziyaret.php'; $birinci = new Ziyaret(); $ikinci = new Ziyaret(); $birinci->artir()->artir(); echo $birinci->durum(), ' ', $ikinci->durum(), "\n";
ziyaret=2 ziyaret=0
durum() trait’te yazılmıştı ama $this->ad() ile sınıfın yazdığı yöntemi çağırdı; sayac ise her nesne için ayrı kaldı. Soyut yöntemi yazmayan bir sınıf trait’i kullanamaz.
# soyut_yok.sh — trait'in soyut yontemi yazilmazsa sinif bildirilemez cat > Eksik.php <<'PHP' <?php trait Sayilabilir { abstract public function ad(): string; } class Eksik { use Sayilabilir; } PHP php -d display_errors=0 Eksik.php; echo "cikis=$?"
cikis=255
Görünürlük ve Takma Ad
as anahtar sözcüğü yalnız çakışmayı çözmek için değildir. Tek başına kullanılınca bir trait yönteminin görünürlüğünü kullanan sınıfta değiştirir; bir ad eklenince aynı yönteme ikinci bir ad ve görünürlük verir. Trait yönteminin özgün adı ve özgün görünürlüğü böylece sınıfın dışına kapatılırken yeni ad açık bırakılabilir.
<?php // gorunurluk.php — as ile gorunurluk degisir ve takma ad eklenir trait Kayitli { public function ozet(): string { return 'kayit'; } } class Gizli { use Kayitli { ozet as protected; ozet as public kayitOzet; } } $gizli = new Gizli(); echo $gizli->kayitOzet(), "\n"; try { $gizli->ozet(); } catch (Error $e) { echo get_class($e), "\n"; }
kayit Error
Aynı yöntem iki adla durdu: kayitOzet() açıktı, ozet() ise korunmuş olduğu için dışarıdan çağrılınca Error verdi.
Öncelik Sırası ve Kalıtımdan Farkı
Aynı adda yöntem birden fazla yerden geliyorsa sıra şöyledir: sınıfın kendi yöntemi, trait yöntemi, üst sınıfın yöntemi. Trait bu yüzden üst sınıfın yöntemini bastırabilir, ama sınıfın kendi yazdığını bastıramaz.
<?php // oncelik.php — sinif, trait ve ust sinif ayni adi tasirsa kazanan class Temel { public function selam(): string { return 'temel'; } } trait Nazik { public function selam(): string { return 'trait'; } } class Alt extends Temel { use Nazik; } class Ozel extends Temel { use Nazik; public function selam(): string { return 'sinif'; } } echo (new Alt())->selam(), ' ', (new Ozel())->selam(), "\n";
trait sinif
Alt kendi selam() yöntemini yazmadı ve trait kazandı; Ozel yazdı ve sınıf kazandı. Üst sınıf iki durumda da sonda kaldı.
Trait kalıtımın yerine geçmez, çünkü aralarında üç fark vardır. Birincisi, trait bir tip değildir: instanceof ile sorulamaz ve parametre tipi olamaz. İkincisi, yöntemler kullanan sınıfa kopyalanır, bu yüzden aynı trait’i kullanan iki sınıfın statik özelliği ayrı kopyalardır. Üçüncüsü, trait’in bir üst sınıfı yoktur; ilişkili bir hiyerarşi kurmaz, yalnız gövde taşır.
<?php // statik_trait.php — trait'in statik ozelligi kullanan sinif basina ayri bir kopyadir trait Sayac { public static int $adet = 0; public static function artir(): int { return ++self::$adet; } } class Birinci { use Sayac; } class Ikinci { use Sayac; } Birinci::artir(); Birinci::artir(); echo Birinci::$adet, ' ', Ikinci::$adet, "\n";
2 0
Birinci iki kez artırdı ve Ikinci sıfırda kaldı: ortak paylaşım isteniyorsa trait değil sınıf statik özelliği ya da bir üst sınıf gerekir. Kalıtım “bu bir şeydir” ilişkisini kurar, trait “bu şu davranışı da taşır” ilişkisini; kod tekrarını gidermek için kalıtım kullanmak ilişkiyi yanlış kurar ve Kalıtım ve Soyut Sınıflar dersindeki kırılgan üst sınıf sorununu büyütür.
Trait ve Arayüzü Birlikte Kullanmak
Trait tip olmadığı için, paylaşılan gövdenin tip olarak da görünmesi isteniyorsa arayüz ve trait birlikte kullanılır: arayüz sözleşmeyi ve tipi, trait ise gövdeyi verir. Sınıf arayüzü implements ile bildirir, yöntemin gövdesini use ile alır.
<?php // birlikte.php — arayuz tipi verir, trait govdeyi saglar interface Etiketli { public function etiket(): string; } trait EtiketGovdesi { public function etiket(): string { return 'etiket'; } } class Gonderi implements Etiketli { use EtiketGovdesi; } $gonderi = new Gonderi(); var_dump($gonderi instanceof Etiketli); echo $gonderi->etiket(), "\n";
bool(true) etiket
Gonderi hem Etiketli tipindedir hem de gövdeyi tekrar yazmamıştır. Bu birleşim, Arayüzler dersindeki sözleşmeyi, bu dersteki paylaşımla buluşturur; ama trait’e sonradan eklenen bir yöntemin kullananı durdurma riski aynen kalır.
Ölçüm Çekirdeği
Bu dersin ölçümü de yayilim.php dosyasını kullanır; çekirdek Sınıflar ve Nesneler dersinde satır satır açıklanmıştı.
<?php // yayilim.php — bir degisikligin nereye ulastigini olcen yardimcilar function yaz(string $dizin, array $dosyalar): void { foreach ($dosyalar as $ad => $icerik) { if ($icerik === null) { continue; // null: dosya silindi } $yol = "$dizin/$ad"; if (!is_dir(dirname($yol))) { mkdir(dirname($yol), 0777, true); } file_put_contents($yol, $icerik); } } function oku(string $dizin): array { $dosyalar = []; $gezgin = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dizin, FilesystemIterator::SKIP_DOTS) ); foreach ($gezgin as $dosya) { $ad = substr($dosya->getPathname(), strlen($dizin) + 1); $dosyalar[$ad] = file_get_contents($dosya->getPathname()); } return $dosyalar; } function kostur(string $dizin): array { $komut = 'cd ' . escapeshellarg($dizin) . ' && php -d display_errors=0 main.php 2>/dev/null'; exec($komut, $satirlar, $kod); return [$kod, implode("\n", $satirlar)]; } function olc(string $ad, array $taban, array $degisiklik, array $baglanti = []): string { if (array_key_exists('main.php', $degisiklik)) { throw new LogicException("$ad: kullanan duzenlenmez"); } yaz("olcum/$ad/once", $taban); yaz("olcum/$ad/sonra", array_merge($taban, $degisiklik)); [$kodOnce, $ciktiOnce] = kostur("olcum/$ad/once"); [$kodSonra, $ciktiSonra] = kostur("olcum/$ad/sonra"); if ($kodOnce !== 0) { throw new LogicException("$ad: taban calismadi"); } $sinif = match (true) { $kodSonra !== 0 => 'kiran', $ciktiOnce !== $ciktiSonra => 'ortuk', array_intersect(array_keys($degisiklik), $baglanti) !== [] => 'baglantida', default => 'yerinde', }; printf( "%-18s %d dosya cikis=%-3d cikti %-6s -> %s\n", $ad, count($degisiklik), $kodSonra, $ciktiOnce === $ciktiSonra ? 'ayni' : 'farkli', $sinif ); return $sinif; }
Trait’e Yöntem Eklenir
Ölçülen değişiklik, Zamanli trait’ine Kayitli ile aynı adlı bir ozet() yöntemi eklemektir. Kullanan Belge sınıfı iki trait’i use eder ve ozet(), saat() değerlerini yazdırır; Belge ve main.php düzenlenmez, düzenlenen tek dosya trait dosyasıdır.
<?php // trait.php — kullanilan trait'e ayni adli bir yontem eklenir require_once 'yayilim.php'; olc('trait-cakismasi', [ 'izler.php' => <<<'PHP' <?php trait Kayitli { public function ozet(): string { return 'kayit'; } } trait Zamanli { public function saat(): string { return '12:00'; } } PHP, 'main.php' => <<<'PHP' <?php require 'izler.php'; class Belge { use Kayitli, Zamanli; } $belge = new Belge(); echo $belge->ozet(), ' ', $belge->saat(), "\n"; PHP, ], [ 'izler.php' => <<<'PHP' <?php trait Kayitli { public function ozet(): string { return 'kayit'; } } trait Zamanli { public function saat(): string { return '12:00'; } public function ozet(): string { return 'zaman'; } } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/trait-cakismasi/$asama"); echo $asama, ' cikis=', $kod, ' cikti=', $cikti, "\n"; }
trait-cakismasi 1 dosya cikis=255 cikti farkli -> kiran once cikis=0 cikti=kayit 12:00 sonra cikis=255 cikti=
NM15. Kullanan dosyada tek satır değişmedi ve çıkış 255 oldu. Çakışma use satırında doğar: iki trait aynı adda yöntem getirince kullanan sınıf bildirilemez. Bu, çakışma çözümünün neden kullanana ait olduğunun bedelidir; trait yazarı yeni bir yöntem eklerken kimin use ettiğini göremez. Dersin ölçü sınıfı kırandır.
Sınırlayıcı Ölçüm
Her ekleme kırar mı? Eğer kullanan sınıf zaten kendi ozet() yöntemini taşıyorsa, trait’in aynı adlı yöntemleri arasındaki çakışma sınıfa ulaşmaz: sınıfın kendi yöntemi trait yöntemlerinin hepsini bastırır.
<?php // sinifin_kendisi.php — sinif ozet()'ini kendisi yazar, trait'e ayni ekleme yapilir require_once 'yayilim.php'; olc('sinifin-kendisi', [ 'izler.php' => <<<'PHP' <?php trait Kayitli { public function ozet(): string { return 'kayit'; } } trait Zamanli { public function saat(): string { return '12:00'; } } PHP, 'main.php' => <<<'PHP' <?php require 'izler.php'; class Belge { use Kayitli, Zamanli; public function ozet(): string { return 'belge'; } } $belge = new Belge(); echo $belge->ozet(), ' ', $belge->saat(), "\n"; PHP, ], [ 'izler.php' => <<<'PHP' <?php trait Kayitli { public function ozet(): string { return 'kayit'; } } trait Zamanli { public function saat(): string { return '12:00'; } public function ozet(): string { return 'zaman'; } } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/sinifin-kendisi/$asama"); echo $asama, ' cikis=', $kod, ' cikti=', $cikti, "\n"; }
sinifin-kendisi 1 dosya cikis=0 cikti ayni -> yerinde once cikis=0 cikti=belge 12:00 sonra cikis=0 cikti=belge 12:00
NM16. Aynı ekleme, aynı trait dosyası; bu kez sınıf yerinde kaldı ve çıktı belge 12:00 olarak sürdü. Kırılma ozet() adının sınıfa iki trait’ten birden ulaşmasından doğuyordu; sınıfın kendi tanımı o ulaşmayı kesti. Yani trait’e eklemenin kırıp kırmayacağı trait’te değil, kullananın ozet() adını zaten sahiplenip sahiplenmediğinde yatıyor.
Özet
- Trait yöntemleri
useile bir sınıfa katar, bir sınıf birden çok trait kullanabilir ve trait bir tip değildir. - İki trait aynı adı getirirse kullanan sınıf
insteadofile seçer,asile ikinci yönteme yeni ad verir. - Kullanılan trait’e çakışan bir
ozet()eklenince kullanan dosya hiç değişmeden durdu, çıkış 255; bu dersin ölçü sınıfı kırandır. - Kullanan sınıf
ozet()yöntemini kendisi yazıyorsa aynı ekleme yerinde kaldı. - Çakışma çözümü kullanana aittir, bu yüzden trait yazarı yeni yöntem eklerken sınıfları görmez.
Sonraki Adım
Trait’ler yöntemleri paylaştırıyordu ama durum ve sabitler başka bir konudur. Statik Üyeler ve Sabitler dersi sınıfa bağlı üyeleri soruyor: bir nesne yaratma yolunu statik bir fabrika yöntemine taşımak ve yapıcıya bir parametre eklemek kullananı neden durdurmuyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.