İçeriğe geç
academia.sh

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 use ile 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 insteadof ile seçer, as ile 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat