Motor bileşenleri için yapıcı ve yıkıcıya mantık koymak yerine neden ayrı başlatma ve temizleme yöntemleri kullanmalıyım?


9

Kendi oyun motorum üzerinde çalışıyorum ve şu anda menajerlerimi tasarlıyorum. Ben bellek yönetimi için kullanma Init()ve CleanUp()fonksiyonları yapıcılar ve yıkıcılar kullanmak daha iyi olduğunu okudum .

Bu işlevlerin nasıl çalıştığını ve bunları motoruma nasıl uygulayabildiğimi görmek için C ++ kod örnekleri arıyordum. Nasıl çalışır Init()ve CleanUp()çalışır ve bunları motoruma nasıl uygulayabilirim?



C ++ için, bkz. Stackoverflow.com/questions/3786853/… Init () kullanmanın temel nedenleri 1) Yardımcı işlevlerle kurucuda istisnaları ve çökmeleri önleme 2) Türetilmiş sınıftan sanal yöntemleri kullanabilme 3) Circumvent dairesel bağımlılıklar 4) kod yinelemesini önlemek için özel bir yöntem olarak
brita_ 19:17

Yanıtlar:


12

Aslında oldukça basit:

Kurulumunuzu yapan bir Yapıcıya sahip olmak yerine,

// c-family pseudo-code
public class Thing {
    public Thing (a, b, c, d) { this.x = a; this.y = b; /* ... */ }
}

... hiç veya çok az şey yapmak senin yapıcı olması ve adlı bir yöntem yazmak .initveya .initializesizin yapıcı normalde ne yapacağını yapardı.

public class Thing {
    public Thing () {}
    public void initialize (a, b, c, d) {
        this.x = a; /*...*/
    }
}

Şimdi sadece şöyle gitmek yerine:

Thing thing = new Thing(1, 2, 3, 4);

Gidebilirsin:

Thing thing = new Thing();

thing.doSomething();
thing.bind_events(evt_1, evt_2);
thing.initialize(1, 2, 3, 4);

Bunun yararı, artık bağımlılık-enjeksiyon / kontrolün ters çevrilmesini sistemlerinizde daha kolay kullanabilmenizdir.

Demek yerine

public class Soldier {
    private Weapon weapon;

    public Soldier (name, x, y) {
        this.weapon = new Weapon();
    }
}

Sen asker inşa edebilirsiniz, ona bir donatmak yöntemini vermek el ona silah ve SONRA yapıcı fonksiyonlarının geri kalan tüm diyoruz.

Şimdi, bir askerin tabancasına, diğerinin tüfeğe, diğerinin de av tüfeğine sahip olduğu düşmanları alt sınıflara ayırmak yerine, tek fark bu diyebilirsiniz:

Soldier soldier1 = new Soldier(),
        soldier2 = new Soldier(),
        soldier3 = new Soldier();

soldier1.equip(new Pistol());
soldier2.equip(new Rifle());
soldier3.equip(new Shotgun());

soldier1.initialize("Bob",  32,  48);
soldier2.initialize("Doug", 57, 200);
soldier3.initialize("Mike", 92,  30);

Yıkımla aynı anlaşma. Özel gereksinimleriniz varsa (olay dinleyicilerini kaldırma, dizilerden örnekleri / birlikte çalıştığınız yapıları vb. Kaldırma), daha sonra bunları manuel olarak çağırırsınız, böylece programın ne zaman ve nerede olduğunu tam olarak bilirsiniz.

DÜZENLE


Kryotan'ın işaret ettiği gibi, bu, orijinal yayının "Nasıl" yanıtını verir , ancak "Neden" için gerçekten iyi bir iş çıkarmaz.

Yukarıdaki cevapta görebileceğiniz gibi, arasında çok fazla bir fark olmayabilir:

var myObj = new Object();
myObj.setPrecondition(1);
myObj.setOtherPrecondition(2);
myObj.init();

ve yazma

var myObj = new Object(1,2);

sadece daha büyük bir yapıcı işlevine sahip.
15 veya 20 ön koşulu olan, bir kurucuyu çalışmak çok zorlaştıracak ve bu şeyleri arayüze çekerek işlerin görülmesini ve hatırlanmasını kolaylaştıracak bir argüman var. , böylece somutlaştırmanın nasıl çalıştığını, bir seviye daha yüksek olduğunu görebilirsiniz.

Nesnelerin isteğe bağlı konfigürasyonu bunun doğal bir uzantısıdır; isteğe bağlı olarak, nesneyi çalıştırmadan önce arabirimdeki değerleri ayarlama.
JS'nin bu fikir için, daha güçlü yazılan c-benzeri dillerde yerinden çıkmış gibi görünen bazı harika kısayolları var.

Bununla birlikte, şansınız, kurucunuzda bu kadar uzun bir argüman listesi ile uğraşıyorsanız, nesnenizin çok büyük ve olduğu gibi çok fazla şey yapmasıdır. Yine, bu kişisel tercihli bir şeydir ve uzak ve geniş istisnalar vardır, ancak bir nesneye 20 şey geçiriyorsanız, daha küçük nesneler yaparak o nesneyi daha az yapmanın bir yolunu bulma şansınız yüksektir. .

Daha uygun bir neden ve yaygın olarak uygulanabilir bir neden, bir nesnenin başlatılmasının şu anda sahip olmadığınız asenkron verilere dayanmasıdır.

Nesneye ihtiyacınız olduğunu biliyorsunuz, bu yüzden yine de oluşturacaksınız, ancak düzgün çalışabilmesi için sunucudan veya şimdi yüklemesi gereken başka bir dosyadan verilere ihtiyacı var.

Yine, gerekli verileri devasa bir başlığa geçiriyor olsanız da, bir arayüz oluşturuyor olsanız da, nesneniz ve sisteminizin tasarımı için önemli olduğu kadar, kavram için gerçekten önemli değildir ...

Ancak nesneyi oluşturma açısından, böyle bir şey yapabilirsiniz:

var obj_w_async_dependencies = new Object();
async_loader.load(obj_w_async_dependencies.async_data, obj_w_async_dependencies);

async_loader bir dosya adı veya bir kaynak adı ya da her neyse, bu kaynağı yükleyebilir - belki ses dosyaları veya görüntü verileri yükler ya da kaydedilmiş karakter istatistiklerini yükler ...

... ve sonra bu verileri geri besleyecekti obj_w_async_dependencies.init(result);.

Bu tür dinamikler web uygulamalarında sıklıkla bulunur.
Bir nesnenin yapısında, daha üst düzey uygulamalar için gerekli değildir: örneğin, galeriler hemen yüklenebilir ve başlatılabilir ve daha sonra fotoğrafları akış olarak görüntüleyebilir - bu gerçekten asenkron başlatma değil, ancak daha sık görüldüğü yer JavaScript kitaplıklarında.

Bir modül diğerine bağlı olabilir ve bu nedenle bu modülün başlatılması, bağımlıların yüklenmesi tamamlanana kadar ertelenebilir.

Bunun oyuna özel örnekleri açısından gerçek bir Gamesınıf düşünün .

Neden arayamam .startya .runoluşturucu içinde?
Kaynakların yüklenmesi gerekiyor - her şeyin geri kalanı hemen hemen tanımlandı ve iyi gidiyor, ancak oyunu bir veritabanı bağlantısı olmadan veya dokular, modeller veya sesler veya seviyeler olmadan çalıştırmayı denersek, özellikle ilginç bir oyun ...

... öyleyse, tipik olarak gördüklerimiz arasındaki farktır Game, ancak "devam et" yöntemini .init, yüklemeyi ayırmak için daha ilginç olan (veya tersine, başlatmayı daha da parçalayın, yüklü olan şeyleri ayarlama ve her şey ayarlandığında programı çalıştırma).


2
" daha sonra bunları manuel olarak çağırırsınız, böylece programda ne zaman ve nerede olduğunu tam olarak bilirsiniz. " C ++ 'ta yıkıcı dolaylı olarak çağrılacak tek zaman bir yığın nesnesi (veya global) içindir. Yığın tahsis edilen nesneler açık imha gerektirir. Bu nedenle, nesnenin ne zaman yeniden yerleştirildiği açıktır.
Nicol Bolas

6
Farklı silah türlerinin enjekte edilmesini sağlamak için bu ayrı yönteme ihtiyacınız olduğunu veya alt sınıfların çoğalmasını önlemenin tek yolu olduğunu söylemek tam olarak doğru değildir. Silah örneklerini kurucudan geçirebilirsiniz! Yani bu bence bir -1 çünkü zorlayıcı bir kullanım örneği değil.
Kylotan

1
-1 Benden de, Kylotan ile hemen hemen aynı nedenlerden dolayı. Çok zorlayıcı bir argüman yapmıyorsunuz, bütün bunlar yapıcılarla yapılabilirdi.
Paul Manta

Evet, inşaatçılar ve yıkıcılar ile gerçekleştirilebilir. Bir tekniğin kullanım durumlarını ve nasıl çalıştıkları veya neden yaptıkları yerine neden ve nasıl istediğini sordu. Ayarlayıcı / ciltleme yöntemlerine sahip olduğunuz bileşen tabanlı bir sisteme sahip olmak, DI için yapıcıdan geçirilmiş parametrelere karşı gerçekten arayüzünüzü nasıl oluşturmak istediğinize iner. Ancak nesneniz 20 IOC bileşeni gerektiriyorsa, TÜMÜNÜ yapıcısına koymak ister misiniz? Yapabilir misin? Tabi ki yapabilirsin. Sen-meli? Belki, belki değil. Eğer yapmamayı seçerseniz .init, belki de değil, muhtemelen bir şeye ihtiyacınız var mı ? Ergo, geçerli dava.
Norguard

1
@Kylotan Aslında nedenini sormak için sorunun başlığını düzenledim. OP sadece "nasıl" sordu. Soruyu, "neden" programlama hakkında bir şey bilen herkes için önemsiz olduğu için "neden" i içerecek şekilde genişlettim ("Sadece mantığınızı ctor içerisine ayrı bir işleve taşıyın ve" deyin) ve "neden" daha ilginç / genel.
Tetrad

17

Init ve CleanUp'ın daha iyi olduğunu söyleyen her ne okursanız, nedenini de söylemeliydiniz. İddialarını haklı göstermeyen makaleler okumaya değmez.

Ayrı başlatma ve kapatma işlevlerine sahip olmak, sistemleri kurmayı ve yok etmeyi kolaylaştırabilir, çünkü onları hangi sırayla arayacağınızı seçebilirsiniz, oysa inşaatçılar tam olarak nesne oluşturulduğunda çağrılır ve yıkıcılar nesne yok edildiğinde çağrılır. 2 nesne arasında karmaşık bağımlılıklarınız olduğunda, genellikle kendilerini ayarlamadan önce her ikisinin de var olmasına ihtiyacınız vardır - ancak bu genellikle başka bir yerde kötü tasarımın bir işaretidir.

Referans sayma ve çöp toplama, nesnenin ne zaman imha edileceğini bilmeyi zorlaştırdığından, bazı dillerde güvenebileceğiniz yıkıcılar yoktur. Bu dillerde neredeyse her zaman bir kapatma / temizleme yöntemine ihtiyacınız vardır ve bazıları simetri için init yöntemini eklemeyi sever.


Teşekkür ederim, ancak makalede bulunmadığı için esas olarak örnekler arıyorum. Sorumun bu konuda belirsiz olup olmadığı için özür dilerim, ama şimdi düzenledim.
Friso

3

Bence en iyi neden: havuza izin vermek.
Init ve CleanUp ürününüz varsa, bir nesne öldürüldüğünde CleanUp'ı çağırıp nesneyi aynı türden bir nesne yığınına itebilirsiniz: 'havuz'.
Daha sonra, yeni bir nesneye ihtiyacınız olduğunda, havuzdan bir nesne patlatabilirsiniz VEYA havuz boşsa - çok kötü- yeni bir tane oluşturmanız gerekir. Sonra bu nesne üzerinde Init çağırırsınız.
İyi bir strateji, oyun 'iyi' sayıda nesne ile başlamadan önce havuzu önceden doldurmaktır, bu nedenle oyun sırasında asla havuzlanmış bir nesne yaratmanız gerekmez.
Öte yandan, 'yeni' kullanırsanız ve herhangi bir işe yaramadığında bir nesneye başvurmayı bırakırsanız, bir zamanlar hatırlanması gereken çöpler yaratırsınız. Bu hatırlama özellikle Javascript gibi tek iş parçacıklı diller için kötü bir şeydir, burada çöp toplayıcı artık kullanılmayan nesnelerin belleğini hatırlaması gerektiğini değerlendirdiğinde tüm kodu durdurur. Oyun birkaç milisaniye boyunca askıda kalır ve oyun deneyimi bozulur.
- Zaten anladınız -: tüm nesnelerinizi bir araya toplarsanız, herhangi bir hatırlama olmaz, dolayısıyla rastgele yavaşlama olmaz.

Havuzdan gelen bir nesneye init çağırmak, bellek + yeni bir nesneyi başlatmaktan çok daha hızlıdır.
Ancak, hız geliştirmenin daha az önemi vardır, çünkü genellikle nesne oluşturma bir performans darboğazı değildir ... Çılgın oyunlar, parçacık motorları veya hesaplamaları için yoğun 2D / 3d vektörler kullanan fizik motoru gibi birkaç istisna dışında. Burada hem hız hem de çöp yaratma havuzu kullanılarak büyük ölçüde geliştirildi.

Rq: Init () her şeyi sıfırlarsa, havuzlanmış nesneleriniz için bir CleanUp yöntemine sahip olmanız gerekmeyebilir.

Düzenleme: bu gönderiyi yanıtlamak beni Javascript'te havuz hakkında yaptığım küçük bir makaleyi sonuçlandırmak için motive etti .
İlgileniyorsanız burada bulabilirsiniz:
http://gamealchemist.wordpress.com/


1
-1: Bunu bir nesne havuzuna sahip olmak için yapmanıza gerek yok. Bunu, yeni yerleşim ve yeniden yerleştirme yoluyla tahsisi açık bir yıkıcı çağrısı ile silinmekten ayırarak yapıdan ayırabilirsiniz. Bu nedenle, yapıcıları / yıkıcıları bazı başlatıcı yöntemlerinden ayırmak için geçerli bir neden değildir.
Nicol Bolas

yeni yerleşim C ++ 'a özgüdür ve biraz ezoteriktir.
Kylotan

+1 bunu c + 'da başka bir şekilde yapmak mümkün olabilir. Ama diğer dillerde değil ... ve bu muhtemelen gameobjects üzerinde Init yöntemini kullanmamın tek nedeni.
Kikaimaru

1
@Nicol Bolas: Sanırım aşırı tepki veriyorsun. Havuz oluşturmanın başka yollarının olması (C ++ 'a özgü karmaşık bir yöntemden bahsediyorsunuz), ayrı bir Init kullanmanın, birçok dilde havuz oluşturmanın güzel ve basit bir yolu olduğu gerçeğini geçersiz kılmaz. tercihlerim GameDev'de daha genel cevaplara gidiyor.
GameAlchemist

@VincentPiel: Yerleşim C ++ 'ta yeni ve böyle bir "karmaşık" nasıl kullanılıyor? Ayrıca, bir GC dilinde çalışıyorsanız, nesnelerin GC tabanlı nesneler içerme olasılıkları iyidir. Öyleyse her birini yoklamak zorunda kalacaklar mı? Bu nedenle, yeni bir nesne yaratmak, havuzlardan bir sürü yeni nesne almayı içerecektir.
Nicol Bolas

0

Sorunuz tersine döndü ... Tarihsel olarak, daha ilgili soru şudur:

Neden olduğu inşaat + intialisation conflated , yani neden yok biz bu adımları ayrı ayrı mı? Şüphesiz bu aykırı SoC ?

C ++ için, RAII'nin amacı, kaynak edinme ve serbest bırakmanın doğrudan kaynak ömrüne bağlanmasıdır; Yapar? Kısmen. Yığın tabanlı / otomatik değişkenler bağlamında% 100 yerine getirilir, burada ilişkili kapsamdan ayrılmak otomatik olarak yıkıcıları çağırır / bu değişkenleri serbest bırakır (dolayısıyla niteleyici automatic). Bununla birlikte, yığın değişkenleri için, yıkıcıyı deleteçalıştırmak için hala açıkça çağırmaya zorlandığınız için, bu çok kullanışlı model üzücü bir şekilde bozulur ve bunu unutursanız, RAII'nin çözmeye çalıştığı şeylerden hala ısırılırsınız; öbeğe ayrılan değişkenler bağlamında, C ++, C ( deletevsfree()) inşaatı aşağıdakiler açısından olumsuz etkileyen başlatma ile sınırlandırırken:

C'de oyunlar / simülasyonlar için bir nesne sistemi oluşturmak, C ++ ve daha sonraki klasik OO dillerinin yaptığı varsayımları daha iyi anlayarak RAII ve diğer bu OO merkezli modellerin sınırlamalarına büyük miktarda ışık tutacağı için şiddetle tavsiye edilir. (C ++ 'ın C içinde yerleşik bir OO sistemi olarak başladığını unutmayın).

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.