Java / C # neden RAII uygulayamıyor?


29

Soru: Java / C # neden RAII uygulayamıyor?

Açıklama: Çöp toplayıcının deterministik olmadığını biliyorum. Dolayısıyla, geçerli dil özellikleri ile bir nesnenin Dispose () yönteminin kapsam çıkışında otomatik olarak çağrılması mümkün değildir. Ancak böyle belirleyici bir özellik eklenebilir mi?

Benim anlayış:

RAII uygulamasının iki gereksinimi karşılaması gerektiğini düşünüyorum:
1. Bir kaynağın ömrü bir kapsama bağlı olmalıdır.
2. Örtük. Kaynağın serbest bırakılması, programcının açık bir ifadesi olmadan gerçekleşmelidir. Açık bir ifade olmadan hafızayı boşaltan bir çöp toplayıcısına benzer. “Açıklık” sadece sınıfın kullanım noktasında meydana gelir. Sınıf kütüphanesi yaratıcısı elbette açıkça bir destructor veya Dispose () yöntemini uygulamalıdır.

Java / C #, 1. noktayı karşılar. C # 'da, IDisposable'ı uygulayan bir kaynak, "kullanma" kapsamına bağlanabilir:

void test()
{
    using(Resource r = new Resource())
    {
        r.foo();
    }//resource released on scope exit
}

Bu, 2. noktayı karşılamaz. Programcı, nesneyi özel bir "kullanma" kapsamına açıkça bağlamalıdır. Programcılar, bir sızıntı yaratarak kaynağı açıkça bir kapsam ile ilişkilendirmeyi unuturlar (ve yaparlar).

Aslında "using" blokları, derleyici tarafından try-finally-dispose () koduna dönüştürülür. Try-finally-dispose () modelinin aynı özelliğine sahiptir. Örtük bir salıverme olmadan, kapsamın kancası sözdizimsel şekerdir.

void test()
{
    //Programmer forgot (or was not aware of the need) to explicitly
    //bind Resource to a scope.
    Resource r = new Resource(); 
    r.foo();
}//resource leaked!!!

Java / C # dilinde, bir akıllı işaretçi ile yığına bağlı özel nesnelere izin veren bir dil özelliği oluşturmaya değeceğini düşünüyorum. Bu özellik, bir sınıfı kapsama bağlı olarak işaretlemenize izin verir, böylece her zaman yığına bir kanca ile oluşturulur. Farklı akıllı işaretçiler için seçenekler olabilir.

class Resource - ScopeBound
{
    /* class details */

    void Dispose()
    {
        //free resource
    }
}

void test()
{
    //class Resource was flagged as ScopeBound so the tie to the stack is implicit.
    Resource r = new Resource(); //r is a smart-pointer
    r.foo();
}//resource released on scope exit.

Bence dolaysızlığın "buna değer" olduğunu düşünüyorum. Çöp toplama işleminin ima ettiği gibi "buna değer". Açık kullanım blokları gözler üzerinde canlandırıcıdır, ancak try-finally-dispose () yöntemine göre anlamsal bir avantaj sağlamaz.

Böyle bir özelliğin Java / C # dillerine uygulanması pratik değil mi? Eski kodu bozmadan tanıtılabilir mi?


3
Pratik değil, imkansız . Yıkıcı garanti etmez C # standart / Disposes olan hiç bakılmaksızın tetikleyen nasıl yapıldığından, çalıştırın. Kapsamın sonunda örtülü bir yıkım eklemek buna yardımcı olmaz.
Telastyn

20
@Telastyn Huh? C # standardının şu anda söylediği şeyle alakası yok çünkü o belgeyi değiştirmeyi tartışıyoruz. Tek sorun bunun pratikte olup olmadığı ve mevcut teminat eksikliğiyle ilgili ilginç olan tek şey bu garanti eksikliğinin sebepleri . Bunun usingyerine getirilmesinin garanti Dispose edildiğine dikkat edin (peki, bir istisna atılmadan aniden ölmekte olan sürecin iskonto edilmesi, ki bu noktada tüm temizlik muhtemelen dağılmakta).

4
Java'nın kopyaları Java geliştiricileri bilinçli olarak RAII'den vazgeçti mi? , kabul edilen cevap tamamen yanlış olsa da . Kısa cevap Java referans kullanmasıdır (yığın) semantik yerine değeri (yığın) belirleyici sonlandırma çok yararlı / mümkün değildir bu yüzden, semantik. C # yapar değer anlambilim (var struct), ama bunlar genellikle çok özel durumlar dışında kaçınılır. Ayrıca bakınız .
BlueRaja - Danny Pflughoeft

2
Benzer, kesin değil.
Maniero

Yanıtlar:


17

Böyle bir dil uzantısı düşündüğünüzden çok daha karmaşık ve istilacı olacaktır. Sadece ekleyemezsin

Bir yığına bağlı türdeki bir değişkenin kullanım ömrü sona ererse, başvurduğu Disposenesneyi çağırın

dilin spesifik bölümlerine ve yapılacaklarına new Resource().doSomething()Biraz daha genel ifadelerle çözülebilecek geçici değerler ( ) sorununu görmezden geleceğim , bu en ciddi sorun değil. Örneğin, bu kod bozulur (ve bu tür bir şey genel olarak yapılması imkansız hale gelir):

File openSavegame(string id) {
    string path = ... id ...;
    File f = new File(path);
    // do something, perhaps logging
    return f;
} // f goes out of scope, caller receives a closed file

Artık kullanıcı tanımlı kopya kurucularına (veya kurucuları taşımaya) ihtiyacınız var ve onları her yere çağırmaya başlayın. Bu sadece performans etkileri içermez, aynı zamanda bunları etkin bir şekilde tiplere değer verir, oysa neredeyse diğer tüm nesneler referans türleridir. Java'nın durumunda bu, nesnelerin çalışma şeklinden köklü bir sapmadır. C # 'da daha az (zaten var struct, ancak AFAIK için kullanıcı tanımlı kopya kurucuları yok), ancak yine de bu RAII nesnelerini daha özel kılıyor. Alternatif olarak, lineer tiplerin sınırlı bir versiyonu (bkz. Rust), parametre geçişi de dahil olmak üzere takma yasaklama yasağı pahasına da (Rust benzeri ödünç alınan referansları ve borç kontrolcüsünü benimseyerek daha fazla karmaşıklık eklemek istemediğiniz sürece) sorunu çözebilir.

Teknik olarak yapılabilir, ancak dilde her şeyden çok farklı şeyler kategorisine giriyorsunuz. Bu neredeyse her zaman kötü bir fikirdir; uygulayıcılar (daha ileri vakalar, her bölümde daha fazla zaman / maliyet) ve kullanıcılar (daha fazla kavram, daha fazla hata olasılığı) için sonuçları vardır. Ekstra rahatlığa değmez.


Neden kopyala / taşı yapıcısına ihtiyacınız var? Dosya hala bir referans türü. Bu durumda, bir işaretçi olan f, arayan kişiye kopyalanır ve kaynağın elden çıkarılmasından sorumludur (derleyici dolaylı olarak arayan
tarafa

1
@bigown Her referansı Filebu şekilde ele alırsanız , hiçbir şey değişmez ve Disposeasla çağrılmaz. Her zaman ararsanız Dispose, tek kullanımlık nesnelerle hiçbir şey yapamazsınız. Yoksa bazen imha etmek için bazen de olmayan bir plan mı öneriyorsunuz? Öyleyse, lütfen ayrıntılı olarak açıklayın, size başarısız olduğu durumları anlatayım.

Şimdi ne söylediğini göremiyorum (yanlış olduğunu söylemiyorum). Nesnede referans değil bir kaynak var.
Maniero

Sizi örnek olarak sadece bir geri dönüşe dönüştüren anlayışım, derleyicinin kaynak alımından hemen önce bir deneme (örneğin, satır 3) ve kapsamın sonundan hemen önce atılan bir blok (satır 6) ekleyeceğidir. Burada sorun yok, anlaştın mı? Örnekinize dönün. Derleyici bir aktarım görüyor, buraya try-finally ekleyemedi, ancak arayan kişi bir (pointer)) File nesnesini alacak ve arayan kişinin bu nesneyi tekrar aktarmadığını varsayarak, compiler orada try-finally kalıbı ekleyecektir. Başka bir deyişle, aktarılamayan her tanımlanabilir nesnenin try-finally kalıbını uygulamasına gerek vardır.
Maniero

1
@bigown Başka bir deyişle, Disposebir referans kaçarsa arama ? Kaçış analizi eski ve zor bir sorundur, bu dilde herhangi bir değişiklik yapmadan her zaman işe yaramayacaktır. Bir referans başka bir (sanal) yönteme ( something.EatFile(f);) gönderildiğinde, f.Disposekapsamın sonunda çağrılmalıdır? Evet ise, fdaha sonra kullanmak üzere depolayan arayanları kırarsınız . Değilse Arayan yoksa, kaynağa sızıntı değil saklamak f. Bunu kaldırmanın basit bir yolu, (daha sonra cevabımda daha önce tartıştığım gibi) diğer birçok komplikasyonun ortaya çıktığı doğrusal bir tip sistemdir.

26

Java veya C # için böyle bir uygulamanın uygulanmasındaki en büyük zorluk, kaynak transferinin nasıl çalıştığını tanımlamak olacaktır. Kaynağın ömrünü kapsamın dışına çıkarmak için bir yola ihtiyacınız olacaktır. Düşünmek:

class IWrapAResource
{
    private readonly Resource resource;
    public IWrapAResource()
    {
        // Where Resource is scope bound
        Resource builder = new Resource(args, args, args);

        this.resource = builder;
    } // Uh oh, resource is destroyed
} // Crap, there's no scope for IWrapAResource we can bind to!

Daha da kötüsü, bunun uygulayıcısı için açık olmayabilir IWrapAResource:

class IWrapSomething<T>
{
    private readonly T resource; // What happens if T is Resource?
    public IWrapSomething(T input)
    {
        this.resource = input;
    }
}

C # 'nın usingifadesi gibi bir şey muhtemelen, kaynak sayma kaynaklarına başvurmadan veya C veya C ++ gibi her yerde değer anlamını zorlamadan RAII semantiğine sahip olacağınız kadar yakındır. Java ve C # bir çöp toplayıcısı tarafından yönetilen kaynakların örtülü paylaşımı olduğundan, bir programcı yapabilmek gerekir asgari bir kaynak tam olarak ne olduğu, bağlı olduğu kapsamını seçmektir usingzaten yok.


Kapsam dışına çıktıktan sonra bir değişkene başvurmanız gerekmediğini varsayarak (ve gerçekten böyle bir ihtiyaç olmamalıdır), bunun için bir sonlandırıcı yazarak bir nesneyi kendi kendine imha edebileceğinizi hala iddia edebilirim. . Sonlandırıcı, nesnenin çöp toplanmasından hemen önce çağrılır. Bkz. Msdn.microsoft.com/en-us/library/0s71x931.aspx
Robert Harvey

8
@Robert: Doğru yazılmış bir program, finalizatörlerin çalıştığını varsayamaz. blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Billy ONeal

1
Hm. Muhtemelen bu yüzden açıklama yaptılar using.
Robert Harvey,

2
Kesinlikle. Bu, C ++ 'daki acemi bir hata kaynağıdır ve Java / C #' de de olabilir. Java / C # referansı imha edilmek üzere olan bir kaynağa sızdırmaz hale getirme özelliğini ortadan kaldırmaz, ancak hem açık hem de isteğe bağlı olarak programcıya hatırlatır ve ne yapılması gerektiği konusunda bilinçli bir seçim sunar.
Aleksandr Dubinsky,

1
@svick O kadar değil IWrapSomethingbertaraf etmek T. Her kim yarattıysa T, kullanmak using, kendin olmak IDisposableya da geçici bir kaynak yaşam döngüsü planı olsun olmasın , bu konuda endişelenmesi gerekir .
Aleksandr Dubinsky,

13

RAII'nin C # gibi bir dilde çalışamamasının nedeni, ancak C ++ dilinde çalışmasının nedeni, C ++ uygulamasında bir nesnenin gerçekten geçici olup olmadığına (yığında tahsis ederek) veya uzun ömürlü olup olmadığına karar verebilmenizdir. newişaretçileri kullanarak ve kullanarak öbek üzerinde ayırma ).

Yani, C ++ 'da şöyle bir şey yapabilirsiniz:

void f()
{
    Foo f1;
    Foo* f2 = new Foo();
    Foo::someStaticField = f2;

    // f1 is destroyed here, the object pointed to by f2 isn't
}

C # 'da iki durum arasında ayrım yapamazsınız, böylece derleyici nesneyi sonlandırıp sonlandırmama konusunda hiçbir fikre sahip olmaz.

Yapabileceğiniz şey, bir tür özel yerel değişken türünü tanıtmak, alanlara koyamayacağınız vb. Tam olarak ne C ++ / CLI yapar. C ++ / CLI dilinde şöyle bir kod yazarsınız:

void f()
{
    Foo f1;
    Foo^ f2 = gcnew Foo();
    Foo::someStaticField = f2;

    // f1 is disposed here, the object pointed to by f2 isn't
}

Bu temelde aşağıdaki C # ile aynı IL ile derlenir:

void f()
{
    using (Foo f1 = new Foo())
    {
        Foo f2 = new Foo();
        Foo.someStaticField = f2;
    }
    // f1 is disposed here, the object pointed to by f2 isn't
}

Sonuç olarak, C # tasarımcılarının neden RAII'yi eklemediğini tahmin edersem, iki farklı tür yerel değişkene sahip olmanın buna değmeyeceğini düşündüler, çünkü çoğunlukla GC'li bir dilde, deterministik sonlandırma işe yaramadı. genellikle.

* C ++ / CLI olan operatörün karşılığı olmadan . Bunu yapmak, yöntemin sona ermesinden sonra alanın imha edilmiş bir nesneye referans vermesi anlamında “güvensiz” olsa da.&%


1
C #, yıkıcılar structD gibi türler için izin verdiyse önemsiz olarak RAII yapabilirdi .
Jan Hudec

6

Sizi usingbloklarla rahatsız eden şey açıklıklarıysa , C # spec'in kendisini değiştirmek yerine, belki de daha az açıklığa doğru küçük bir adım atabiliriz. Bu kodu düşünün:

public void ReadFile ()
{
  string filename = "myFile.dat";
  local Stream file = File.Open(filename);
  file.Read(blah blah blah);
}

Bkz localkelime ekledim? Tek yaptığı, biraz daha fazla sözdizimsel şeker eklemek, tıpkı usingderleyiciye değişkenin kapsamının sonunda Disposebir finallyblokta çağırmasını söylemek . Hepsi bu. Bu tamamen eşdeğerdir:

public void ReadFile ()
{
  string filename = "myFile.dat";
  using (Stream file = File.Open(filename))
  {
      file.Read(blah blah blah);
  }
}

ancak açık bir kapsamdan ziyade örtük bir kapsam ile. Kapsam önerisi olarak tanımlanan sınıfa sahip olmam gerekmediğinden diğer önerilerden daha basit. Sadece temiz, daha örtük sözdizimsel şeker.

Burada çözülmesi zor olan kapsamlarla ilgili sorunlar olabilir, ancak şu anda göremiyorum, ancak onu bulan herkesi takdir ediyorum.


1
@ mike30, ancak onu tür tanımına taşımak, tam olarak listelenen sorunlara yönlendirir - işaretçiyi farklı bir yönteme geçirirseniz veya işlevden döndürürseniz ne olur? Bu şekilde kapsam belirleme başka bir yerde değil kapsamda ilan edilir. Bir tür Tek Kullanımlık olabilir, ancak Dispose'ı aramak ona bağlı değildir.
Avner Shahar-Kashtan

3
@ mike30: Meh. Tüm bu sözdizimi, parantezleri kaldırmak ve uzantı olarak, sağladıkları kapsam denetimidir.
Robert Harvey,

1
@RobertHarvey Tam olarak. Daha temiz, daha az iç içe geçmiş kod için biraz esneklikten ödün verir. @ Delnan'ın önerisini alır ve usinganahtar kelimeyi yeniden kullanırsak, mevcut davranışı koruyabilir ve belirli bir kapsama ihtiyaç duymadığımız durumlarda da bunu kullanabiliriz. usingGeçerli kapsam için küme ayracı varsayılan yok.
Avner Shahar-Kashtan 30:13

1
Dil tasarımında yarı pratik alıştırmalarla ilgili bir sorunum yok.
Avner Shahar-Kashtan

1
@RobertHarvey. Şu anda C # ile uygulanmayan herhangi bir şeye karşı bir önyargınız var gibi görünüyor. C # 1.0'dan memnun olsaydık, jenerik, linq, use-blocks, ipmlicit tipleri vb. Olmazdı. Bu sözdizimi, dolaylılık sorununu çözmez, ancak mevcut kapsamı bağlamak için iyi bir şekerdir.
mike30

1

RAII'nin çöp toplayan bir dilde nasıl çalıştığına bir örnek olarak with, Python'da anahtar kelimeyi kontrol edin . Deterministik olarak tahrip edilmiş nesnelere güvenmek yerine, belirli bir sözcüksel kapsamla ilişkilendirmenize __enter__()ve __exit__()yöntemlere izin vermenize izin verir . Yaygın bir örnek:

with open('output.txt', 'w') as f:
    f.write('Hi there!')

C ++ 'nın RAII stilinde olduğu gibi,' normal 'bir çıkış, a break, bir ani returnya da bir istisna olsa da, o bloktan çıkarken dosya kapatılır .

Not open()çağrı zamanki dosya açma fonksiyonudur. Bu işi yapmak için, döndürülen dosya nesnesi iki yöntem içerir:

def __enter__(self):
  return self
def __exit__(self):
  self.close()

Bu Python'da ortak bir deyimdir: bir kaynakla ilişkilendirilen nesneler tipik olarak bu iki yöntemi içerir.

Dosya nesnesinin __exit__()aramadan sonra hala ayrılmaya devam edebileceğini unutmayın , önemli olan şey kapalı olmasıdır.


7
withPython'dakiler neredeyse aynen usingC # 'daki gibidir ve bu kadarıyla RAII değil.

1
Python'un "with" kapsamına bağlı kaynak yönetimidir ancak bir akıllı göstericinin zımni olduğu eksiktir. Bir işaretçiyi akıllı olarak bildirme eylemi "açık" olarak kabul edilebilir, ancak derleyici, nesneler türünün bir parçası olarak akıllılığı zorlarsa, "örtük" e dayanır.
mike30

AFAICT, RAII'nin amacı kaynaklara sıkı bir şekilde yer açmaktır. eğer sadece nesnelerin serbest bırakılmasıyla ilgileniyorsanız, hayır, çöp toplanan diller bunu yapamaz. Sürekli olarak kaynakları serbest bırakmakla ilgileniyorsanız, bu onu yapmanın bir yoludur (bir diğeri deferGo dilindedir).
Javier

1
Aslında, Java ve C # 'nın açık inşaatları şiddetle desteklediğini söylemenin adil olduğunu düşünüyorum. Aksi halde, neden Arayüzler ve miras kullanımında doğabilecek tüm törenle uğraşmak?
Robert Harvey,

1
@delnan, Go 'üstü kapalı' arayüzlere sahiptir.
Javier,
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.