boş işaretçiler vs.


27

Atıf: Bu, ilgili bir P.SE sorusundan ortaya çıktı

Arkaplanım C / C ++ dilinde, ancak Java'da adil bir miktarda çalıştım ve şu anda C # kodluyorum. C arka planım nedeniyle, geçen ve iade edilen işaretçilerin kontrol edilmesi ikinci eldir, ancak bakış açımın önyargılı olduğunu kabul ediyorum.

Kısa bir süre önce fikrin bir nesnenin her zaman döndürüldüğü Null Nesne Örüntüsünden bahsettiğini gördüm . Normal durum, beklenen, doldurulmuş nesneyi ve hata durumu, boş bir işaretçi yerine boş nesneyi döndürür. Çağıran işlevin erişmesi için her zaman bir tür nesneye sahip olacağı ve bu nedenle boş erişim belleği ihlallerinden kaçınılması gerektiği öncül.

  • Öyleyse, Null Nesne Örüntüsünü kullanma yerine null çekin artıları / eksileri nelerdir?

NOP ile daha temiz arama kodu görebiliyorum, ancak başka türlü ortaya çıkmayan gizli hataları nerede yaratacağını da görebiliyorum. Uygulamamın, vahşi doğada sessiz bir hatadan kaçmak yerine geliştirirken sert bir şekilde (bir istisna) başarısız olmasını tercih ederim.

  • Boş Nesne Deseni, boş bir denetim yapmamaya benzer sorunlar yaşayamaz mı?

Çalıştığım nesnelerin çoğu, kendi nesnelerini veya kaplarını tutar. Ana nesnenin kaplarının hepsinin kendilerine ait boş nesneler olduğunu garanti etmek için özel bir vakamın olması gerekecek gibi görünüyor. Birden fazla iç içe geçme katmanıyla çirkinleşebilir gibi görünüyor.


"Hata durumu" değil, "her durumda geçerli bir nesne".

@ ThorbjørnRavnAndersen - iyi nokta ve bunu yansıtmak için soruyu düzenledim. NOP'ın öncülünü hala kaçırıyorsam lütfen bana bildirin.

6
Kısa cevap, "boş nesne kalıbı" nın bir yanlış isim olduğudur. Daha doğrusu "boş nesne karşıtı kalıp" olarak adlandırılır. Genellikle sınıfın bütün arayüzünü uygulayan geçerli bir nesne olan "hata nesnesi" nden daha iyisindir, ancak bunun herhangi bir kullanımı yüksek, ani ölüme neden olur.
Jerry Coffin

1
@ JerryCoffin bu durumda bir istisna ile belirtilmelidir. Buradaki fikir, asla bir null elde edemeyeceğiniz ve dolayısıyla null değerini kontrol etmeniz gerekmez.

1
Bu "kalıbı" sevmiyorum. Ancak, muhtemelen tüm yabancı anahtarlar tamamen boş olmadığında, son güncellemeden önce, düzenlenebilir verilerin düzenlenmesi sırasında yabancı anahtarlarda özel "boş satırlara" başvurmanın daha kolay olduğu, aşırı sınırlı ilişkisel veritabanlarına dayanmaktadır.

Yanıtlar:


24

Feci bir başarısızlık olduğu için boş (veya Boş Nesne) döndürülen yerlerde Boş Nesne Kalıbı kullanmazsınız. Bu yerlerde boş dönmeye devam edecektim. Bazı durumlarda, iyileşme olmazsa, en azından kaza dökümü sorunun tam olarak nerede gerçekleştiğini göstereceğinden, çökmeye de neden olabilirsiniz. Bu gibi durumlarda, kendi hata işlemenizi eklediğinizde, hala işlemi sonlandıracaksınız (yine, kurtarma işleminin olmadığı durumlar için söyledim), ancak hata işlemeniz bir çökme dökümü için sağlanan çok önemli bilgileri gizleyecektir.

Boş Nesne Deseni, nesnenin bulunamadığı bir durumda alınabilecek varsayılan davranışların olduğu yerler içindir. Örneğin, aşağıdakileri göz önünde bulundurun:

User* pUser = GetUser( "Bob" );

if( pUser )
{
    pUser->SetAddress( "123 Fake St." );
}

Eğer NOP kullanıyorsanız, şunu yazarsınız:

GetUser( "Bob" )->SetAddress( "123 Fake St." );

Bu kodun davranışının "Bob varsa, adresini güncellemek istiyorum" olduğunu unutmayın. Belli ki, başvurunuz Bob'un mevcut olmasını gerektiriyorsa, sessizce başarılı olmak istemezsiniz. Ancak bu tür davranışların uygun olacağı durumlar vardır. Ve bu durumlarda, NOP daha temiz ve özlü bir kod üretmiyor mu?

Bob olmadan yaşayamayacağınız yerlerde, GetUser () 'a daha yüksek bir düzeyde ele alınacak ve genel işlem başarısızlığını bildirecek bir uygulama istisnası (yani, erişim ihlali ya da benzeri bir şey) atmadım. Bu durumda, NOP'a gerek yoktur ancak NULL için açıkça kontrol edilmesine de gerek yoktur. IMO, NULL için kontrol eder, sadece kodu daha büyük yapar ve okunabilirliği elinden alır. NULL için kontrol edin hala bazı arayüzler için doğru tasarım seçimidir, ancak neredeyse bazılarının düşünme eğilimi göstermez.


4
Boş nesne kalıpları için kullanım durumuyla aynı fikirde. Fakat felaketle ilgili başarısızlıklarla karşılaştığınızda neden boşa dönün? Neden istisnalar atmıyorsunuz?
Apoorv Khurasia

2
@MonsterTruck: Bunu tersine çevirin. Kod yazarken, harici bileşenlerden gelen girdilerin çoğunu doğruladığımdan emin olun. Ancak, iç sınıflar arasında, bir sınıfın bir fonksiyonunun asla NULL döndürmeyeceği bir kod yazarsam, o zaman arayan tarafa sadece "daha güvenli olmak" için null kontrolünü ekleyemem. Bu değerin muhtemelen NULL olabilmesinin tek yolu, uygulamamda feci bir mantık hatası olmuşsa. Bu durumda, programımın tam olarak bu noktada çökmesini istiyorum, çünkü daha sonra güzel bir çökme dökümü dosyası alıyorum ve mantık hatasını belirlemek için yığını ve nesne durumunu tersine çeviriyorum.
DXM

2
@MonsterTruck: "tersine çevir" demek istediğim, felaketle ilgili bir hata tanımladığınızda açıkça NULL döndürmeyin, ancak NULL'un yalnızca öngörülemeyen felaket bir hata nedeniyle ortaya çıkmasını bekleyin.
DXM

Artık felaket bir hata ile ne demek istediğinizi anlıyorum - nesne tahsisatının kendisinin başarısız olmasına neden olan bir şey. Sağ? Düzenleme: Takma adınız yalnızca 3 karakter uzunluğunda olduğundan @ DXM yapamazsınız.
Apoorv Khurasia

@MonsterTruck: "@" olayıyla ilgili garip. Başkalarının daha önce yaptığını biliyorum. Acaba web sitesini son zamanlarda değiştirmişler mi? Tahsis edilmesi gerekmez. Bir sınıf yazarsam ve API'nin amacı her zaman geçerli bir nesne döndürmekse, arayanın boş bir denetim yapmasını istemezdim. Ancak feci hata, programcı hatası (NULL'un geri dönmesine neden olan bir yazım hatası) veya belki de zamanından önce bir nesneyi silen bazı yarış koşulları veya belki bazı yığın bozulması olabilir. Temel olarak, NULL'un döndürülmesine yol açan beklenmeyen bir kod yolu. Bu olduğunda istediğin ...
DXM

7

Öyleyse, Null Nesne Örüntüsünü kullanma yerine null çekin artıları / eksileri nelerdir?

Artıları

  • Daha fazla vakayı çözdüğü için boş bir kontrol daha iyidir. Tüm nesnelerin mantıklı ve varsayılan olmayan bir davranışı yoktur.
  • Boş bir kontrol daha sağlamdır. Aklı başında varsayılanları olan nesneler bile aklı başında varsayılanın geçerli olmadığı yerlerde kullanılır. Başarısız olacaksa, kod köke yakın başarısız olmalıdır. Başarısız olacaksa kodun açıkça başarısız olması gerekir.

Eksileri

  • Sane varsayılanları genellikle daha temiz kodla sonuçlanır.
  • Aklı başında yer alan varsayılanlar, vahşi yaşamayı başardıklarında genellikle daha az yıkıcı hatalara neden olur.

Bu son "profesyonel", her birinin ne zaman uygulanması gerektiğine ilişkin (benim deneyimlerime göre) ana farklılıktır. "Arıza gürültülü mü olmalı?" Bazı durumlarda, zor ve acil olmamak bir başarısızlık istersiniz; Asla gerçekleşmemesi gereken bir senaryo varsa. Eğer hayati bir kaynak bulunmadıysa… vb. Bazı durumlarda, gerçekten bir hata olmadığı için aklı başında bir öneriye sahip olursunuz : bir sözlükten değer almak, ancak örneğin anahtar eksik.

Diğer tasarım kararlarında olduğu gibi, ihtiyaçlarınıza bağlı olarak olumsuz ve olumsuz yönleri vardır.


1
Artılar bölümünde: İlk maddeyi kabul edin. İkinci nokta tasarımcıların hatasıdır çünkü boş olması gerekmemesine rağmen null kullanılabilir. Kalıpla ilgili kötü bir şey söyleme, sadece deseni yanlış kullanan geliştiriciye yansır.
Apoorv Khurasia

Daha catstrophic başarısızlık nedir, para gönderememek veya alıp para almadan gönderememek (ve dolayısıyla artık paraya sahip olmamak), açık bir kontrol öncesi bilmeden?
user470365

Eksileri: Sane varsayılanları yeni nesneler oluşturmak için kullanılabilir. Boş olmayan bir nesne döndürülürse, başka bir nesne oluştururken bazı nitelikleri değiştirilebilir. Boş bir nesne döndürülürse, yeni bir nesne oluşturmak için kullanılabilir. Her iki durumda da aynı kod çalışır. Kopyala-değiştir ve yenileri için ayrı bir koda gerek yok. Bu, felsefenin bir parçasıdır, her kod satırının böcekler için başka bir yer olduğu; çizgilerin azaltılması hataları azaltır.
shawnhcorey

@shawnhcorey - ne?
Telastyn

Boş bir nesne gerçek bir nesneymiş gibi kullanılabilir olmalıdır. Örneğin, IEEE'nin NaN'sini düşünün (bir sayı değil). Bir denklem yanlış giderse, döndürülür. Ve üzerinde yapılan herhangi bir işlem, NaN döndüreceğinden, daha sonraki hesaplamalar için kullanılabilir. Her aritmetik işlemden sonra NaN kontrolü yapmanız gerekmediğinden kodu basitleştirir.
shawnhcorey

6

Ben iyi bir nesnel bilgi kaynağı değilim, ama öznel olarak:

  • Sistemimin ölmesini hiç istemiyorum; bana göre bu kötü tasarlanmış veya eksik bir sistemin işaretidir; Komple sistem olası tüm durumları ele almalı ve asla beklenmedik duruma gelmemelidir; Bunu başarmak için tüm akışları ve durumları modellemede açık olmam gerekiyor; null kullanmak zaten açık değildir ve bir umrella altında birçok farklı olayı gruplandırır; NonExistingUser nesnesini null değerine tercih ediyorum, NaN'i null değerine tercih ediyorum, ... bu tür bir yaklaşım bu durumlar ve onların istediğim kadar ayrıntıyla ele alınmalarına izin vermeme izin verirken, null beni yalnızca null seçeneğiyle bırakır; Java gibi ortamlarda herhangi bir nesne null olabilir, öyleyse neden bu genel dava havuzunda gizlice kendi durumunuzu gizlemeyi tercih edersiniz?
  • boş uygulamalar nesneden oldukça farklı davranışlara sahiptir ve çoğunlukla tüm nesneler grubuna yapıştırılmıştır (neden? neden? neden?); Bana göre bu kadar ciddi bir fark, null'ların tasarımının bir sonucu gibi görünüyor ki, bu durumda sistemin büyük olasılıkla öleceği (açıkça belirtilmediği sürece pek çok insanın aslında sistemlerini ölmesini tercih ediyorum); bana göre kökünde kusurlu bir yaklaşım var - açıkça ilgilenmediğiniz sürece sistemin varsayılan olarak ölmesine izin veriyor, bu çok güvenli değil mi? ancak her durumda, değeri ve nesneye aynı şekilde davranan bir kod yazmayı imkansız kılar - yalnızca birkaç temel yerleşik işleç (atamak, eşit, param, vb.) çalışır, diğer tüm kodlar başarısız olur ve ihtiyacınız olur. iki tamamen farklı yol;
  • Null Object ölçeklerini daha iyi kullanmak - içine istediğiniz kadar bilgi ve yapı koyabilirsiniz; NonExistingUser çok iyi bir örnektir - almaya çalıştığınız bir kullanıcının bir e-postasını içerebilir ve bu bilgilere dayanarak yeni bir kullanıcı oluşturmayı önerebilir. boş sonuç işleme yakın;

Java veya C # gibi ortamlarda size açıklığın temel faydasını vermeyecektir - çözümün güvenliği tamamlanmıştır, çünkü sistemdeki herhangi bir nesnenin yerine hala boş alabilirsiniz ve Java'nın hiçbir yolu yoktur (Fasulye için özel ek açıklamalar hariç) , vb.) Açıkça belirtilen durumlar dışında sizi bu konuda korumak Ancak, sistemde boş uygulamalar bulunmadığından, sorun çözümüne bu şekilde yaklaşmak (istisnai durumları temsil eden nesnelerle) belirtilen tüm faydaları sağlar. Dolayısıyla, ana dillerden başka bir şey hakkında okumaya çalışın ve kendinizi kodlama yöntemlerinizi değiştirirken bulacaksınız.

Genel olarak Null / Error nesneleri / geri dönüş türleri çok katı hata işleme stratejisi ve oldukça matematiksel olarak kabul edilirken, Java veya C istisnaları işleme stratejisi "ASAP" stratejisinden yalnızca bir adım öteye gider - bu nedenle temelde tüm yükü bırakmak sistemin geliştirici veya bakımcıdaki dengesizliği ve öngörülemezliği.

Ayrıca, bu stratejiden daha üstün olarak kabul edilebilecek monadlar da var (ilk başta anlayışın karmaşıklığı konusunda da üstün), ancak bunlar Null Object içsel yönleri modellerken ihtiyaç duyduğunuz durumlar hakkında. Yani, NonExistingUser muhtemelen monad belki_existing olarak modellenmiştir.

Son olarak, Null Nesne kalıbı ile sessizce hata durumlarının ele alınması arasında bir bağlantı olduğunu sanmıyorum. Etrafında başka bir yol olmalı - her bir hata vakasını açık bir şekilde modellemeli ve buna göre ele almalı. Şimdi elbette özensiz olmaya karar verebilirsiniz (ne sebeple olursa olsun) ve hata durumunun açıkça ele alınmasını ancak genel olarak ele almayı atlayabilirsiniz.


9
Beklenmeyen bir şey olduğunda başvurumun başarısız olmasının nedeni, beklenmedik bir şey olduğu. Nasıl oldu? Niye ya? Bir kilitlenme dökümü alıyorum ve kodda gizlenmektense, tehlikeli olabilecek bir sorunu araştırabilir ve çözebilirim. C #, tabii ki uygulamayı denemek ve kurtarmak için tüm beklenmeyen istisnaları yakalayabilirim, ancak o zaman bile sorunun yaşandığı yeri günlüğe kaydetmek ve ayrı işlem gerektiren bir şey olup olmadığını öğrenmek istiyorum (olmasa bile "etmeyin" Bu hatayı kaydet, aslında beklenen ve kurtarılabilir ").
Luaan

3

Bir Boş Nesne'nin yaşama kabiliyetinin, dilinizin statik olarak yazılmış olup olmamasına bağlı olarak biraz farklı göründüğünü not etmek ilginçtir. Ruby , Null (veya Nildilin terminolojisinde) için bir nesne uygulayan bir dildir.

Başlatılmamış belleğe bir işaretçi geri almak yerine, dil nesneyi döndürür Nil. Bu, dilin dinamik olarak yazılmış olması ile kolaylaştırılmıştır. Bir fonksiyonun daima geri dönmesi garanti edilmez int. Yapması gereken Nilbuysa geri dönebilir . Bunu statik olarak yazılmış bir dilde yapmak daha karmaşık ve zor hale gelir, çünkü doğal olarak referans olarak adlandırılabilecek herhangi bir nesneye uygulanabilir olmasını beklersiniz. Boş olabilecek olabilecek herhangi bir nesnenin boş sürümlerini uygulamaya başlamalısınız (veya Scala / Haskell rotasına gidin ve bir tür "Belki" sarmalayıcıya sahip olmalısınız).

MrLister, C ++ 'ta ilk oluşturduğunuzda bir başlangıç ​​referansı / işaretçisini başlatmanızın garanti edilmediği gerçeğini ortaya koydu. İşlenmemiş boş işaretçi yerine özel bir boş nesne kullanarak, bazı güzel ek özellikler edinebilirsiniz. Ruby'de boş metinle sonuçlanan Nilbir to_s(toString) işlevi bulunur. Bu, bir dizgede boş bir geri dönüş almak istemezseniz ve kilitlenmeden raporlamayı atlamak istiyorsanız harikadır. Açık Sınıflar ayrıca, Nilgerekirse çıktısını el ile geçersiz kılmanıza da olanak sağlar .

Verilen tüm tuz bir tanesiyle alınabilir. Dinamik bir dilde boş nesnenin doğru kullanıldığında büyük kolaylık sağlayabileceğine inanıyorum. Ne yazık ki, bu programdan en iyi şekilde yararlanmanız, temel standartlarını düzenlemenizi gerektirebilir ve bu da programınızın daha standart formlarıyla çalışan insanlar için anlaşılmasını ve sürdürülmesini zorlaştırabilir.


1

Bazı ek bakış açıları için, bir Null nesnesi yerine nil türünün bir nesne gibi çalıştığı, Objective-C'ye bir bakın. Bir nil nesnesine gönderilen herhangi bir mesajın etkisi yoktur ve yöntemin dönüş değeri türü ne olursa olsun 0 / 0.0 / NO (yanlış) / NULL / nil değerini döndürür. Bu, MacOS X ve iOS programlarında çok yaygın bir desen olarak kullanılır ve genellikle yöntemler, 0 döndüren bir sıfır nesnesi makul bir sonuç olacak şekilde tasarlanır.

Örneğin, bir dizgede bir veya daha fazla karakter olup olmadığını kontrol etmek istiyorsanız,

aString.length > 0

ve mevcut olmayan bir dize herhangi bir karakter içermediğinden aString == nil ise makul bir sonuç alın. İnsanlar buna alışmak için biraz zaman alır, ancak başka dillerde her yerde sıfır değerleri kontrol etmeye alışmam biraz zaman alır.

(Ayrıca NSNull sınıfının oldukça farklı davranış gösteren ve pratikte pek kullanılmayan tek bir nesne var).


Objective-C, Null Object / Null Pointer olayını gerçekten bulanıklaştırdı. nilolsun #defineboş nesne gibi NULL'a ve davranacaktır 'd. C # / Java boş işaretçilerinde hata yayarken bu bir çalışma zamanı özelliğidir.
Maxthon Chan,

0

Sakıncalıysanız, ikisini de kullanmayın. Bir seçenek türü, bir değer veya özel bir toplam türü döndüren bir fabrika işlevi kullanın. Başka bir deyişle, varsayılan olarak garanti edilmemiş bir değerden farklı bir türde potansiyel olarak varsayılan değeri temsil eder.

Öncelikle desiderata'yı listeleyelim, sonra bunu birkaç dilde nasıl ifade edebileceğimizi düşünelim (C ++, OCaml, Python)

  1. Derleme zamanında varsayılan bir nesnenin istenmeyen kullanımını yakalamak.
  2. Verilen bir değerin kodu okurken potansiyel olarak varsayılan olup olmadığını açıkça belirtin.
  3. mümkünse her tür için bir kez mantıklı varsayılan değerimizi seçin . Kesinlikle her arama sitesinde potansiyel olarak farklı bir varsayılan seçmeyin .
  4. Statik analiz araçlarının veya bir insanın grepolası hatalar için avlanmasını kolaylaştırır .
  5. Bazı uygulamalarda , beklenmedik şekilde varsayılan bir değer verilirse program normal şekilde devam etmelidir. Diğer uygulamalarda , ideal olarak bilgilendirici bir şekilde, varsayılan bir değer verilirse program derhal durmalıdır.

Boş Nesne Deseni ile boş işaretçiler arasındaki gerilimin (5) geldiğini düşünüyorum. Hataları yeterince erken tespit edersek, (5) tartışılmaz hale gelir.

Bu dili dile göre düşünelim:

C ++

Bence, bir C ++ sınıfı genellikle kütüphanelerle etkileşimi kolaylaştırdığı ve sınıfı kaplarda kullanmayı kolaylaştıracağı için varsayılan olarak yapılandırılabilir olmalıdır. Aynı zamanda kalıtımı kolaylaştırır, çünkü hangi süper sınıf yapıcısını arayacağınızı düşünmeniz gerekmez.

Ancak bu, bir tür değerin MyClass"varsayılan durumda" olup olmadığından emin olamayacağınız anlamına gelir . Çalışma bool nonemptysüresinde yüzey varsayılanlığı değerine benzer bir alana koymanın yanı sıra , yapabileceğiniz en iyi şey , kullanıcıyı kontrol etmeye zorlayacak şekilde yeni örnekler üretmektirMyClass .

Bir döndüren bir fabrika işlevini kullanarak öneriyoruz std::optional<MyClass>, std::pair<bool, MyClass>bir veya bir r değeri referans std::unique_ptr<MyClass>mümkünse.

Fabrika işlevinizin, varsayılan olarak oluşturulmuş olandan farklı bir tür "yer tutucu" durumu döndürmesini istiyorsanız, a MyClasskullanın std::pairve işlevinizin bunu yaptığını belgelemeye dikkat edin.

Eğer fabrika fonksiyonu benzersiz bir isme sahipse grep, isim için kolaylık ve isim bulması kolaydır . Ancak, grepprogramcının fabrika işlevini kullandığı, ancak kullanmadığı durumlar için zordur .

OCaml

OCaml gibi bir dil kullanıyorsanız, o zaman bir optiontür (OCaml standart kütüphanesinde) veya bir eithertür kullanabilirsiniz (standart kütüphanede değil, kendi dilinizi kolayca kullanabilirsiniz). Veya bir defaultabletür (Terim uyduruyorum defaultable)

type 'a option =
    | None
    | Some of 'a

type ('a, 'b) either =
    | Left of 'a
    | Right of 'b

type 'a defaultable =
    | Default of 'a
    | Real of 'a

defaultableYukarıda gösterilen A , bir çiftten daha iyidir, çünkü kullanıcının, deseni çıkarmak için eşleşmesi gerekir 'ave çiftin ilk öğesini görmezden gelemez.

defaultableYukarıda gösterilen tipe eşdeğer std::variant, aynı tipte iki örneği olan bir c kullanarak C ++ 'da kullanılabilir , ancak std::varianther iki tip de aynıysa API'nin parçaları kullanılamaz. Ayrıca std::variant, tür kurucularının isimlendirilmemesi nedeniyle garip bir kullanımdır .

piton

Python için herhangi bir derleme zamanı denetimi alamazsınız. Ancak, dinamik olarak yazıldığından, derleyiciyi yerleştirmek için bir tür yer tutucu örneğine ihtiyaç duyduğunuz durumlar genellikle yoktur.

Varsayılan bir örnek oluşturmaya zorlandığınızda sadece bir istisna atmanızı tavsiye ederim.

Bu kabul edilebilir değilse, DefaultedMyClassmiras alan bir şeyi yaratmanızı MyClassve fabrika işlevinizden geri almanızı öneririm . İhtiyaç duyduğunuzda, varsayılan durumlarda "inatçılık" işlevselliği açısından size esneklik kazandırır.

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.