Değişken yapılar neden “şeytani”?


485

Burada SO ile ilgili tartışmaların ardından, değişebilir yapıların “kötü” (bu sorunun cevabında olduğu gibi) olduğuna dair birkaç kez okudum .

C # 'daki değişebilirlik ve yapılarla ilgili asıl sorun nedir?


12
Değişken yapıların kötülük olduğunu iddia etmek değişebilir ints, bools iddia etmek gibidir ve diğer tüm değer türleri kötülüktür. Değişebilirlik ve değişmezlik vakaları vardır. Bu durumlar, bellek ayırma / paylaşma türüne değil, verilerin oynadığı role bağlıdır.
Slipp D.Thompson

41
@slipp intve boolvardır değil değişken ..
Blorgbeard çıktı

2
.-Syntax, ref-tipi veriler ve değer-tipi verilerle işlemlerin belirgin şekilde farklı olmalarına rağmen aynı görünmesini sağlar. Bu yapıların değil C # 'ın özelliklerinin bir hatasıdır - bazı diller a[V][X] = 3.14yerinde mutasyon yapmak için alternatif bir sözdizimi sunar . C # 'da,' MutateV (Eylem <ref Vector2> mutator) 'gibi yapısal üye mutator yöntemleri sunmak ve bunu gibi kullanmak daha iyi olur a.MutateV((v) => { v.X = 3; }) (örnek, C #' ın refanahtar kelimeyle ilgili kısıtlamaları nedeniyle aşırı basitleştirilmiştir , ancak bazılarıyla geçici çözümler mümkün olmalıdır) .
Slipp D.Thompson

2
@Slipp Bu tür yapıların tam tersini düşünüyorum. Neden DateTime veya TimeSpan (benzerleri gibi) gibi .NET kitaplığında uygulanmış yapıların değişmez olduğunu düşünüyorsunuz? Belki de bu tür yapıların sadece bir üyesini değiştirmek yararlı olabilir, ancak çok rahatsız edici, çok fazla soruna yol açar. Aslında C #, derleyici için derlemediğinden, IL için derler. IL içinde (biz zaten adında değişkeni sağlayarak x: Bu tekli işlem) 4 talimatları olduğunu ldloc.0... 0-indeks değişkeni içine (yükler
Sushi271

1
... yazın. Ttürüdür. Ref, değişkenin bir yönteme aktarılmasını sağlayan bir anahtar kelimedir, bir kopyasının değil. Değişkenleri değiştirebildiğimiz için referans türleri için de bir anlamı vardır , yani yöntem dışındaki referans, yöntem içinde değiştirildikten sonra başka bir nesneye işaret edecektir. Yana ref Tbir tip ama bir yöntem parametresi geçme moda değil, içine koyamazsınız <>sadece türleri vardır konabilir nedeni. Yani bu sadece yanlış. Belki bunu yapmak uygun olurdu, belki C # ekibi bazı yeni sürüm için bunu yapabilirdi, ama şu anda bazı üzerinde çalışıyorlar ...
Sushi271

Yanıtlar:


290

Yapılar değer türleridir, yani çevrildiklerinde kopyalanırlar.

Dolayısıyla, bir kopyayı değiştirirseniz, yalnızca o kopyayı değiştirirsiniz, orijinali değil, etrafındaki diğer kopyaları da değiştirmezsiniz.

Yapınız değişmezse, değere göre iletilmesinden kaynaklanan tüm otomatik kopyalar aynı olacaktır.

Değiştirmek isterseniz, değiştirilmiş verilerle yeni bir yapı örneği oluşturarak bunu bilinçli olarak yapmanız gerekir. (kopya değil)


82
"Yapınız değişmezse, tüm kopyalar aynı olacaktır." Hayır, bu, farklı bir değer istiyorsanız bilinçli olarak bir kopya yapmanız gerektiği anlamına gelir. Bu, orijinali değiştirdiğinizi düşünerek bir kopyayı değiştirerek yakalanmayacağınız anlamına gelir.
Lucas,

19
@Lucas Sanırım farklı bir kopyadan bahsediyorsunuz Değerden geçirilmenin bir sonucu olarak yapılan otomatik kopyalardan bahsediyorum, 'bilinçli olarak yapılmış kopyanız' yanlışlıkla yapmamanız ve gerçekten bir kopya değil, farklı veriler içeren kasıtlı yeni anlar.
trampster

3
Düzenlemeniz (16 ay sonra) bunu biraz daha açık hale getiriyor. Yine de "(değişmez yapı), orijinali değiştirdiğinizi düşünerek bir kopyasını değiştirerek yakalanmayacağınız anlamına gelir".
Lucas

6
@Lucas: Bir yapı bir kopyasını yaparak değiştirerek ve bir şekilde bir (tek bir yapı alanını yazıyor olması kılan orijinali değiştiriyorsa düşünme tehlikesi kendini bariz bir tek kopyasını yazıyor gerçeğini) görünüyor bir sınıf nesnesini, içinde yer alan bilgileri tutmanın bir yolu olarak tutan birinin, kendi bilgisini güncellemek ve işlem sırasında başka bir nesne tarafından tutulan bilgileri bozmak tehlikesine kıyasla oldukça küçüktür.
supercat

2
3. paragraf en iyi ihtimalle yanlış veya net değil. Yapınız değişmezse, onun alanlarını veya yapılan kopyaların alanlarını değiştiremezsiniz. "Eğer değiştirmek istiyorsanız için ... var" çok yanıltıcı sonra, daha değiştiremezsiniz bunu hiç , ne bilinçli veya bilinçsiz bir. İstediğiniz verilerin orijinal kopyayla aynı veri yapısına sahip olmaktan başka bir ilgisi olmayan yeni bir örnek oluşturmak.
Saeb Amini

167

Nereden başlamalı ;-p

Eric Lippert'in blogu her zaman bir teklif için iyidir:

Değişken değer türlerinin kötü olmasının bir başka nedeni de budur. Değer türlerini daima değişmez kılmaya çalışın.

İlk olarak, değişiklikleri kolayca kaybetme eğilimindesiniz ... örneğin, bir şeyleri listeden çıkarmak:

Foo foo = list[0];
foo.Name = "abc";

bu ne değişti? Yararlı bir şey yok ...

Özellikleri ile aynı:

myObj.SomeProperty.Size = 22; // the compiler spots this one

sizi yapmaya zorlar:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

daha az eleştirel olarak, bir boyut sorunu var; değiştirilebilir nesneler birden fazla özelliğe sahip olma eğilimindedir ; yine de iki ints, a string, a DateTimeve a ile bir yapınız boolvarsa, çok fazla bellekle çok hızlı bir şekilde yazabilirsiniz. Bir sınıfla, birden çok arayan aynı örneğe bir referans paylaşabilir (referanslar küçüktür).


5
Evet ama derleyici bu şekilde aptal. Çünkü, mülkiyet-yapı üyelerine atama aptal bir tasarım kararı IMHO olduğunu izin vermeme olduğu için izin ++operatörü. Bu durumda, derleyici sadece programlayıcıyı engellemek yerine açık ödevi yazar.
Konrad Rudolph

12
@Konrad: myObj.SomeProperty.Size = 22, myObj.SomeProperty'nin bir COPY'sini değiştirir. Derleyici sizi bariz bir hatadan kurtarıyor. Ve ++ için izin VERİLMEZ.
Lucas,

@Lucas: Mono C # derleyicisi buna kesinlikle izin veriyor - Windows olmadığım için Microsoft'un derleyicisini kontrol edemiyorum.
Konrad Rudolph

6
@Konrad - daha az dolaylı olarak çalışmalı; "yığın üzerinde sadece geçici bir değer olarak var olan ve hiçlik içinde buharlaşmak üzere olan bir şeyin değerinin mutasyona uğratılması", yani engellenen durumdur.
Marc Gravell

2
@Marc Gravell: Eski kod parçasında, adı "abc" olan ve diğer öznitelikleri Liste [0] 'a ait olan ve Liste [0]' ı rahatsız etmeden bir "Foo" ile sonuçlanırsınız. Foo bir sınıf olsaydı, onu klonlamak ve daha sonra kopyayı değiştirmek gerekirdi. Bana göre, değer türü ve sınıf ayrımı arasındaki büyük sorun "." iki amaç için operatör. Benim druthers'ım olsaydı, sınıflar her ikisini de destekleyebilirdi. ve yöntemler ve özellikler için "->", ancak "." özellikleri, uygun alan değiştirilerek yeni bir örnek oluşturmak olacaktır.
supercat

71

Kötülük demem ama değişebilirlik, programcı tarafından maksimum işlevsellik sağlamak için genellikle aşırı hassasiyetin bir işaretidir. Gerçekte, bu genellikle gerekli değildir ve bu da arabirimi daha küçük, kullanımı daha kolay ve yanlış kullanımı zorlaştırır (= daha sağlam).

Buna bir örnek, yarış koşullarında okuma / yazma ve yazma / yazma çakışmalarıdır. Bunlar sadece değişmez yapılarda meydana gelemez, çünkü yazma geçerli bir işlem değildir.

Ayrıca, iddia mutability aslında gerekli hemen hemen hiçbir zaman , programcı sadece düşünür o olabilir gelecekte. Örneğin, bir tarihi değiştirmek mantıklı değildir. Bunun yerine, eskisini temel alan yeni bir tarih oluşturun. Bu ucuz bir işlemdir, bu nedenle performans dikkate alınmaz.


1
Eric Lippert olduklarını söylüyor ... cevabımı gör.
Marc Gravell

42
Eric Lippert'e saygı duyduğum kadarıyla o Tanrı değil (ya da en azından henüz değil). Bağlandığınız blog gönderisi ve yukarıdaki gönderiniz, yapıları elbette değişmez hale getirmek için makul argümanlardır, ancak aslında değişmez yapıları asla kullanmadıkları için argümanlar olarak çok zayıftırlar . Ancak bu yayın +1.
Stephen Martin

2
C # 'da geliştirme yaparken, genellikle arada bir değişime ihtiyacınız vardır - özellikle akış vb. Mevcut çözümlerle sorunsuz bir şekilde çalışmak istediğiniz İş Modelinizle. Değişebilirlikle
Ricky Helgesson

3
@StephenMartin: Tek bir değeri çevreleyen yapılar genellikle değişmez olmalıdır, ancak yapılar, "özdeşliği" bulunmayan bağımsız ama ilişkili değişkenlerin (bir noktanın X ve Y koordinatları gibi) sabit kümelerini kapsüllemek için açık ara en iyi ortamdır. grubudur. Bu amaçla kullanılan yapılar değişkenlerini genel alan olarak göstermelidir. Bu tür amaçlar için bir sınıftan ziyade bir sınıf kullanmanın daha yanlış olduğu fikrini düşünürdüm. Değişmez sınıflar genellikle daha az verimlidir ve değişebilir sınıflar genellikle korkunç anlambilime sahiptir.
supercat

2
@StephenMartin: Örneğin, floatbir grafik dönüşümünün altı bileşenini döndürmesi gereken bir yöntem veya özellik düşünün . Böyle bir yöntem altı bileşenli bir açık alan yapısı döndürürse, yapı alanlarının değiştirilmesinin alındığı grafik nesnesini değiştirmeyeceği açıktır. Böyle bir yöntem değişken bir sınıf nesnesi döndürürse, belki de özelliklerini değiştirmek temeldeki grafik nesnesini değiştirir ve belki de olmaz - kimse gerçekten bilmez.
supercat

48

Değişken yapılar kötü değildir.

Yüksek performans koşullarında kesinlikle gereklidirler. Örneğin, önbellek hatları ve / veya çöp toplama bir darboğaz haline geldiğinde.

Bu mükemmel geçerli kullanım durumlarında "kötü" değişmez bir yapının kullanılmasını istemem.

C # sözdiziminin bir değer türünün veya bir başvuru türünün bir üyesinin erişimini ayırt etmeye yardımcı olmadığı noktasını görebiliyorum, bu yüzden değişmez yapıları tercih edebilecek, değişmez yapıları zorlayan yapıları tercih ediyorum .

Bununla birlikte, sadece değişmez yapıları "kötü" olarak etiketlemek yerine, dili kucaklamanızı ve daha yararlı ve yapıcı başparmak kurallarını savunmanızı öneririm.

Örneğin: "yapılar varsayılan olarak kopyalanan değer türleridir. Kopyalamak istemiyorsanız bir referans gerekir" veya "önce salt okunur yapılarla çalışmayı deneyin" .


8
Ayrıca, bir değişken setini koli bandı ile birlikte sabitlemek isterse, değerlerinin ayrı olarak veya bir ünite olarak işlenebilmesi veya saklanabilmesi durumunda, derleyiciden sabit bir dizi structaynı amaçlara ulaşmak için beceriksizce kullanılabilecek bir sınıfı tanımlamak ya da böyle bir sınıfı taklit etmek için bir yapıya bir demet önemsiz şey eklemek yerine değişkenleri birlikte (yani kamusal alanlarla a beyan etmek ) koli bandı ile birbirine yapışmış bir dizi değişken gibi
davranın

41

Herkese açık değiştirilebilir alanları veya özellikleri olan yapılar kötü değildir.

"This" i mutasyona uğratan yapısal yöntemler (özellik ayarlayıcılarından farklı olarak) biraz kötülüktür, çünkü .net bunları yapmayan yöntemlerden ayırmanın bir yolunu sağlamaz. "Bu" ifadesini değiştirmeyen yapı yöntemleri, savunma amaçlı kopyalamaya gerek kalmadan salt okunur yapılarda bile çalıştırılabilir olmalıdır. "Bunu" değiştiren yöntemler salt okunur yapılarda hiç çalıştırılmamalıdır. .Net, "bu" değerini değiştirmeyen yapı yöntemlerinin salt okunur yapılarda çağrılmasını engellemek istemediğinden, salt okunur yapıların değiştirilmesine izin vermek istemediğinden, yapıları defalarca sadece bağlamlar, tartışmalı olarak her iki dünyanın da kötüleşmesi.

Bununla birlikte, salt okunur bağlamlarda kendi kendini değiştiren yöntemlerin ele alınmasına ilişkin sorunlara rağmen, değiştirilebilir yapılar genellikle değiştirilebilir sınıf türlerinden çok daha üstün bir anlambilim sunar. Aşağıdaki üç yöntem imzasını göz önünde bulundurun:

struct PointyStruct {genel int x, y, z;};
sınıf PointyClass {public int x, y, z;};

void Yöntem1 (PointyStruct foo);
geçersiz Yöntem2 (ref PointyStruct foo);
void Yöntem3 (PointyClass foo);

Her yöntem için aşağıdaki soruları cevaplayın:

  1. Yöntemin "güvensiz" kod kullanmadığını varsayarsak, foo değiştirebilir mi?
  2. Yöntem çağrılmadan önce 'foo' dış referansları yoksa, daha sonra dış referans var mı?

Yanıtlar:

Soru 1::
Method1()hayır (açık niyet)
Method2() : evet (açık niyet)
Method3() : evet (belirsiz niyet)
Soru 2
Method1():: hayır
Method2(): hayır (güvenli değilse)
Method3() : evet

Yöntem1 foo değiştiremez ve hiçbir zaman başvuru alır. Yöntem2, foo alanlarına kısa ömürlü bir başvuru alır, bu da foo alanlarını herhangi bir sırayla, geri dönene kadar değiştirmek için kullanabilir, ancak bu başvuruyu devam ettiremez. Yöntem2 dönmeden önce, güvenli olmayan kod kullanmadıkça, 'foo' başvurusundan yapılmış olabilecek tüm kopyalar kaybolacaktır. Yöntem3, Yöntem2'den farklı olarak, foo'ya karışık olarak paylaşılabilir bir başvuru alır ve bununla ne yapabileceğini söylemez. Foo'yu hiç değiştirmeyebilir, foo'yu değiştirebilir ve sonra geri dönebilir veya başka bir iş parçacığına foo'ya, gelecekteki keyfi bir zamanda onu rastgele bir şekilde değiştirebilecek bir referans verebilir.

Yapı dizileri harika anlambilim sunar. Rectangle türündeki RectArray [500] verildiğinde, örneğin 123 öğesini eleman 456'ya nasıl kopyalayacağınız açıktır ve daha sonra bir süre sonra eleman 456'yı 556'yı rahatsız etmeden elemanın genişliğini nasıl ayarlayacağınız açıktır. "RectArray [432] = RectArray [321 ]; ...; RectArray [123]. Genişlik = 555; ". Dikdörtgenin Width adında bir tamsayı alanına sahip bir yapı olduğunu bilmek, yukarıdaki ifadeler hakkında herkesin bilmesi gereken birini söyler.

Şimdi RectClass'ın Rectangle ile aynı alanlara sahip bir sınıf olduğunu ve bir RectClass tipi RectClassArray [500] üzerinde aynı işlemleri yapmak istediğini varsayalım. Belki de dizinin değiştirilebilir RectClass nesnelerine önceden başlatılmış 500 değişmez referansı tutması beklenir. bu durumda, uygun kod "RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;" gibi bir şey olacaktır. Belki de dizinin değişmeyecek örnekleri içerdiği varsayılır, bu nedenle uygun kod daha çok "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = Yeni RectClass (RectClassArray [321) ]); RectClassArray [321] .X = 555; " Birinin ne yapması gerektiğini bilmek için, RectClass hakkında çok daha fazla şey bilmek gerekir (örneğin bir kopya oluşturucu, bir kopyadan çıkarma yöntemi vb. ) ve dizinin kullanım amacı. Hiçbir yerde bir yapı kullanmak kadar temiz değil.

Emin olmak için, ne yazık ki, bir dizi dışında herhangi bir kap sınıfı için bir yapı dizisinin temiz semantiğini sunmanın güzel bir yolu yoktur. Bir koleksiyon örneğin bir dizeyle dizine eklenmesini isterse, muhtemelen en iyisi, dizin için bir dize, genel bir parametre ve geçirilecek bir temsilci kabul edecek genel bir "ActOnItem" yöntemi sunmak olacaktır. hem genel parametreye hem de koleksiyon öğesine referans vererek. Bu, yapı dizileriyle neredeyse aynı semantiğe izin verir, ancak vb.net ve C # insanları güzel bir sözdizimi sunmak için takip edilemezse, kod makul performans olsa bile tıknaz görünümlü olacaktır (genel bir parametre geçirir) statik delege kullanılmasına izin verir ve geçici sınıf örnekleri oluşturma gereksinimini önler).

Şahsen ben nefret duydum Eric Lippert ve ark. değişebilir değer türleri ile ilgili azarlama. Her yerde kullanılan karışık referans türlerinden çok daha temiz semantikler sunuyorlar. .Net'in değer türleri desteğiyle ilgili bazı kısıtlamalara rağmen, değişebilir değer türlerinin diğer herhangi bir türden daha uygun olduğu birçok durum vardır.


1
@Ron Warholic: SomeRect'in bir Dikdörtgen olduğu belli değildir. Rectangle'dan örtük olarak tahmin edilebilen başka bir tür olabilir. Her ne kadar, Rectangle'dan örtük olarak tahmin edilebilen tek sistem tanımlı tür RectangleF'dir ve bir RectangleF alanlarını Rectangle yapıcısına geçirmeye çalışırsa derleyici squawk yapar (eski Tek ve ikinci Tamsayı olduğu için) , bu tür örtük tiplendirmelere izin veren kullanıcı tanımlı yapılar olabilir. BTW, ilk ifade, SomeRect bir Rectangle veya RectangleF olsun, eşit derecede iyi çalışır.
supercat

Gösterdiğiniz tek şey, bir örnekte, bir yöntemin daha net olduğuna inanıyorsunuz. İle biz örnek alırsak Rectangleben kolayca yüksek olsun ortak bir sitation ile gelebilir belirsiz davranış. WinForms'un Rectangleformun Boundsözelliğinde kullanılan değiştirilebilir bir tür uyguladığını düşünün . Sınırları değiştirmek istersem güzel sözdizimini kullanmak isterim: form.Bounds.X = 10;Ancak bu formda hiçbir şeyi değiştirmez (ve sizi bilgilendiren hoş bir hata oluşturur). Tutarsızlık, programlama yasağıdır ve bu nedenle değişmezlik istenmektedir.
Ron Warholic

3
Warholic @Ron: BTW, olur gibi söylemek mümkün "form.Bounds.X = 10;" ve sadece çalışmasını sağlayın, ancak sistem bunu yapmak için temiz bir yol sağlamaz. Geri arama kabul yöntemleri olarak değer türü özellikleri gösterme kuralı, sınıfları kullanan herhangi bir yaklaşımdan çok daha temiz, verimli ve doğru olarak doğru kod sunabilir.
supercat

4
Bu cevap, en çok oy alan cevaplardan birkaçından daha anlayışlı. Değişken değer türlerine karşı olan argümanın, örtüşme ve mutasyonu karıştırdığınızda "ne beklediğiniz" fikrine dayanması saçmadır. Bunu yapmak çok kötü bir şey nasıl olsa !
Eamon Nerbonne

2
@supercat: Kim bilir, belki de C # 7 için bahsettikleri ref-return özelliği bu tabanı kapsayabilir (aslında ayrıntılı olarak bakmadım, ancak yüzeysel olarak benzer görünüyor).
Eamon Nerbonne

24

Değer türleri temelde değişmez kavramları temsil eder. Fx, tamsayı, vektör vb. Gibi matematiksel bir değere sahip olmak ve daha sonra değiştirmek mümkün değildir. Bu, bir değerin anlamını yeniden tanımlamak gibidir. Bir değer türünü değiştirmek yerine, başka bir benzersiz değer atamak daha mantıklıdır. Değer türlerinin, özelliklerinin tüm değerlerini karşılaştırarak karşılaştırıldığını düşünün. Mesele şu ki, eğer özellikler aynıysa, o değerin aynı evrensel temsilidir.

Konrad'ın bahsettiği gibi, değer herhangi bir duruma veya bağlama bağımlı bir zaman nesnesinin örneğini değil, zaman içindeki o benzersiz noktayı temsil ettiği için bir tarihi değiştirmek de mantıklı değildir.

Umarım bu sizin için bir anlam ifade eder. Emin olmak için, değer türleriyle yakalamaya çalıştığınız kavram pratik detaylardan ziyade.


Eh, onlar gerektiğini azından ;-p az değişmez kavramları temsil
Marc Gravell

3
Eh, sanırım onlar System.Drawing.Point değişmez hale getirebilirdi ama ciddi bir tasarım hatası IMHO olurdu. Bence noktalar aslında arketipik bir değer türü ve değişebilir. Ve gerçekten yeni başlayan 101 programlamanın ötesinde hiç kimseye sorun çıkarmazlar.
Stephen Martin

3
Prensip olarak, puanların da değişmez olması gerektiğini düşünüyorum, ancak eğer tipi daha zor veya daha az zarif hale getirirse, elbette bu da dikkate alınmalıdır. Kimse kullanmak istemiyorsa en iyi prensipleri destekleyen kod yapılarına sahip olmanın bir anlamı yoktur;)
Morten Christiansen

3
Değer türleri, basit değişmez kavramları temsil etmek için yararlıdır, ancak açık alan yapıları, ilişkili ancak bağımsız değerlerin küçük sabit kümelerini (bir noktanın koordinatları gibi) tutmak veya etrafından geçirmek için kullanılacak en iyi türlerdir. Böyle bir değer türünün depolandığı yer, alanlarının değerlerini içine alır ve başka bir şey içermez. Aksine, değişebilir bir referans tipinin bir depolanma yeri, değişebilir nesnenin durumunu tutmak amacıyla kullanılabilir, fakat aynı zamanda evrende aynı nesnede var olan diğer tüm referansların kimliğini de kapsar.
supercat

4
“Değer türleri temelde değişmez kavramları temsil eder”. Hayır. Değer türünde bir değişkenin en eski ve en kullanışlı uygulamalarından biri int, değiştirilemezse tamamen işe yaramayacak bir yineleyicidir. Bence “değer türleri derleyici / çalışma zamanı uygulamaları” nı “bir değer türüne yazılan değişkenler” ile karıştırıyorsunuz - ikincisi kesinlikle olası değerlerden herhangi birine değiştirilebilir.
Slipp D.Thompson

19

Programcının bakış açısından öngörülemeyen davranışlara yol açabilecek birkaç köşe vakası daha var.

Değişmez değer türleri ve salt okunur alanlar

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this() 
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass 
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass 
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

Değiştirilebilir değer türleri ve dizi

Yapımızın bir dizisine sahip olduğumuzu Mutableve IncrementIbu dizinin ilk öğesi için yöntemi çağırdığımızı varsayalım . Bu çağrıdan nasıl bir davranış bekliyorsunuz? Dizinin değerini mi yoksa yalnızca bir kopyasını mı değiştirmeli?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

Yani, değişebilir yapılar sizin ve ekibin geri kalanı ne yaptığınızı açıkça anladıkça kötü değildir. Ancak program davranışının beklenenden farklı olacağı, üretilmesi zor ve anlaşılması zor hatalara yol açabilecek çok fazla köşe vakası vardır.


5
.Net dilleri biraz daha iyi değer türü desteğe sahip olsaydı ne olurdu, açıkça bu şekilde bildirilmedikçe yapısal yöntemlerin 'bunu' değiştirmesi yasaklanmalı ve bu şekilde bildirilen yöntemlerin salt okunur olarak yasaklanması gerekir. bağlamlarda. Değişken yapı dizileri, başka yollarla verimli bir şekilde elde edilemeyen yararlı anlamlar sunar.
supercat

2
bunlar değişebilir yapılardan kaynaklanacak çok ince sorunların iyi örnekleridir. Bu davranışların hiçbirini beklemezdim. Neden bir dizi size bir referans verir, ancak bir arayüz size bir değer verir? Değerleri her zaman bir kenara (gerçekten beklediğim şey) bir kenara bırakmanın en azından başka bir yol olacağını düşünürdüm: referanslar veren arayüz; değerler veren diziler ...
Dave Cousineau

@Sahuagin: Ne yazık ki, bir arayüzün referans gösterebileceği standart bir mekanizma yoktur. Bir special tanımlayarak böyle şeyler güvenle ve yararlı (örn yapılması gereken izin verebilir .net yolu vardır "ArrayRef <T>" Bir içeren yapı T[]ve bir tamsayı indeksi ve tür bir özelliği için bir erişim koşuluyla ArrayRef<T>bir şekilde yorumlanacaktır uygun bir dizi öğesine erişim) [eğer bir sınıf ArrayRef<T>başka bir amaçla ortaya çıkarmak isterse, onu almak için bir özelliğin aksine - bir yöntem sağlayabilir]. Ne yazık ki, böyle bir hüküm mevcut değildir.
supercat

2
Ah benim ... bu değişebilir yapıları kötülük yapar!
nawfal

1
Bu cevabı beğendim, çünkü çok açık olmayan çok değerli bilgiler içeriyor. Ama gerçekten de, bu iddia edildiği gibi değişebilir yapılara karşı bir argüman değil. Evet, burada gördüğümüz şey Eric'in söylediği gibi bir "çaresizlik çukuru" ama bu çaresizliğin kaynağı değişmez değil. Umutsuzluğun kaynağı, yapıların kendini değiştiren yöntemlerdir . (Dizilerin ve listelerin neden farklı davrandığına gelince, biri temel olarak bir bellek adresini hesaplayan bir operatör ve diğeri bir özelliktir. Bir "referansın" bir adres değeri olduğunu anladığınızda genel olarak her şey netleşir .)
AnorZaken

18

C / C ++ gibi bir dilde programladıysanız, yapıların değiştirilebilir olarak kullanılması iyidir. Sadece onları ref ile geçirin ve yanlış gidebilecek hiçbir şey yok. Bulduğum tek sorun, C # derleyicisinin kısıtlamalarıdır ve bazı durumlarda, aptal şeyi bir Kopya yerine yapıya başvuru kullanmaya zorlayamayız (bir yapı C # sınıfının bir parçası olduğu gibi) ).

Yani, değişken yapılar kötülük, C # etti değildir yapılmış onları kötülük. Değişken yapıları C ++ 'da her zaman kullanıyorum ve çok kullanışlı ve sezgisel. Buna karşılık, C #, nesneleri ele alma biçimi nedeniyle yapıları sınıfların üyesi olarak tamamen terk etmemi sağladı. Onların rahatlığı bize mal oldu.


Yapı türlerinin sınıf alanlarına sahip olmak genellikle çok yararlı bir desen olabilir, ancak bazı kısıtlamalar vardır. Alanlardan ziyade özellikler kullanılırsa veya kullanılırsa performans düşer readonly, ancak bu tür şeyleri yapmaktan kaçınırsa, yapı türlerinin sınıf alanları gayet iyi olur. Yapıların gerçekten temel sınırlaması, değişebilir bir sınıf tipindeki bir yapı alanının, int[]kimliği veya değişmeyen bir değer kümesini kapsülleyebileceği, ancak istenmeyen bir kimliği kapsüllemeden değiştirilebilir değerleri kapsüllemek için kullanılamayacağıdır.
supercat

13

Hangi yapıların amaçlandığına bağlı kalırsanız (işaretçiler olarak kullanılmadığında C #, Visual Basic 6, Pascal / Delphi, C ++ yapı türünde (veya sınıflarında), bir yapının bileşik değişkenten daha fazla olmadığını görürsünüz . Bu şu anlama gelir: bunları ortak bir adla (üyelere başvurduğunuz bir kayıt değişkeni) paketlenmiş bir değişkenler kümesi olarak ele alırsınız.

Bunun birçok insanı OOP için derinden karıştırdığını biliyorum, ancak doğru kullanıldıklarında bu tür şeylerin doğal olarak kötü olduğunu söylemek için yeterli neden yok. Bazı yapılar istedikleri gibi değişmezdir (bu Python'ların durumudur namedtuple), ancak dikkate alınması gereken başka bir paradigmadır.

Evet: yapılar çok fazla bellek içerir, ancak aşağıdakileri yaparak tam olarak daha fazla bellek olmayacaktır:

point.x = point.x + 1

nazaran:

point = Point(point.x + 1, point.y)

Bellek tüketimi en azından aynı veya hatta daha fazla değişmez durumda olacaktır (bu durum, dile bağlı olarak mevcut yığın için geçici olsa da).

Fakat son olarak yapılar yapı değil nesnelerdir. POO'da, bir nesnenin ana özelliği, çoğu zaman bellek adresinden daha fazla olmayan kimliğidir . Struct, veri yapısını ifade eder (uygun bir nesne değildir ve bu nedenle hiçbir şekilde kimliğe sahip değildir) ve veriler değiştirilebilir. Diğer dillerde, kayıt ( Pascal için olduğu gibi struct yerine ) kelimedir ve aynı amacı taşır: dosyalardan okunması, değiştirilmesi ve dosyalara dökülmesi (yalnızca ana kullanın ve birçok dilde, kayıtta veri hizalamasını tanımlayabilirsiniz, ancak düzgün bir şekilde adlandırılan Nesneler için durum böyle değildir).

İyi bir örnek ister misiniz? Yapılar dosyaları kolayca okumak için kullanılır. Python bu kütüphaneye sahiptir çünkü nesne yönelimli olduğu ve yapıları desteklemediği için, kütüphaneyi başka bir şekilde uygulamak zorunda kalmıştır, ki bu biraz çirkin. Yapıları uygulayan diller bu özelliğe sahiptir ... yerleşik. Pascal veya C gibi dillerde uygun bir yapıya sahip bir bitmap başlığı okumayı deneyin. Kolay olacaktır (yapı düzgün bir şekilde oluşturulmuş ve hizalanmışsa; Pascal'da kayıt tabanlı bir erişim kullanmazsınız, ancak rastgele ikili verileri okumak için işlevler kullanırsınız). Bu nedenle, dosyalar ve doğrudan (yerel) bellek erişimi için yapılar nesnelerden daha iyidir. Bugün gelince, JSON ve XML'e alışkınız ve bu nedenle ikili dosyaların kullanımını (ve bir yan etki olarak, yapıların kullanımını) unutuyoruz. Ama evet: varlar ve bir amaçları var.

Onlar kötü değiller. Onları doğru amaç için kullanın.

Çekiç açısından düşünürseniz, vidaları çivi olarak işlemek isteyeceksiniz, vidaların duvara daldırılması daha zor ve vidaların hatası olacak ve kötü olanlar olacaklar.


12

1.000.000 yapıdan oluşan bir diziniz olduğunu düşünün. Bid_price, offer_price (belki ondalık sayılar) ve benzeri şeyler içeren bir özkaynağı temsil eden her yapı, bu C # / VB tarafından oluşturulur.

Dizinin, yönetilmeyen öbekte ayrılan bir bellek bloğunda oluşturulduğunu ve diğer bazı yerel kod dizisinin aynı anda diziye erişebildiğini düşünün (belki de bazı yüksek perf kodlar matematik yapıyor).

C # / VB kodunun fiyat değişikliklerinin piyasa akışını dinlediğini, bu kodun dizinin bazı öğelerine (hangi güvenlik için) erişmek ve daha sonra bazı fiyat alanlarını değiştirmek zorunda olabileceğini düşünün.

Bunun saniyede onlarca hatta yüz binlerce kez yapıldığını düşünün.

Peki, gerçeklerle yüzleşelim, bu durumda bu yapıların değiştirilebilir olmasını istiyoruz, bunun başka bir yerel kod tarafından paylaşıldıkları için olması gerekiyor, böylece kopya oluşturmak yardımcı olmayacak; bu oranlarda 120 baytlık bir yapının bir kopyasını oluşturmak, özellikle de bir güncellemenin sadece bir bayt veya ikiyi etkileyebileceği durumlarda, neyse.

Hugo


3
Doğru, ancak bu durumda bir yapıyı kullanmanın nedeni, uygulama tasarımına dış kısıtlamalar (yerel kodun kullanımı tarafından olanlar) tarafından dayatılmasıdır. Bu nesneler hakkında açıkladığınız her şey, bunların açıkça C # veya VB.NET'te sınıflar olması gerektiğini gösterir.
Jon Hanna

6
Neden bazı insanların olayların sınıf objesi olması gerektiğini düşündüğünden emin değilim. Tüm dizi yuvaları farklı örneklerle başvurulursa, bir sınıf türü kullanmak bellek gereksinimine fazladan on iki veya yirmi dört bayt ekler ve bir sınıf nesnesi başvuru dizisine sıralı erişim, sıralı erişimden çok daha yavaş olmaya eğilimlidir bir dizi yapı.
supercat

9

Bir şey mutasyona uğrayabildiğinde, bir kimlik duygusu kazanır.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Çünkü Persondeğişken, bu düşünecek daha doğal Eric'in pozisyonunu değiştirerek daha Eric klonlama klon hareketli ve orijinali yok . Her iki işlem de içeriğini değiştirmeyi başardı eric.position, ancak biri diğerinden daha sezgisel. Benzer şekilde, Eric'i onu değiştirme yöntemleri için (referans olarak) etrafta dolaşmak daha sezgiseldir. Bir yönteme Eric'in bir klonu vermek neredeyse her zaman şaşırtıcı olacaktır. Mutasyona geçmek isteyen herkes Personreferans göndermeyi hatırlamalı Personya da yanlış bir şey yapacaktır.

Türü değiştirilemez hale getirirseniz sorun ortadan kalkar; eğer değiştiremezsem eric, alıp almamın ericveya klonlamamın hiçbir farkı yoktur eric. Daha genel olarak, bir tür, gözlemlenebilir durumunun tümü aşağıdakilerden biri olan üyelerde tutulursa değere göre geçmek güvenlidir:

  • değişmez
  • referans türleri
  • değere göre güvenli

Bu koşullar yerine getirilirse, değişken bir değer türü referans türü gibi davranır çünkü sığ bir kopya alıcının orijinal verileri değiştirmesine izin verir.

Değişmez bir Personşeyin sezgiselliği, ne yapmaya çalıştığınıza bağlıdır. Bir kişi hakkında Personsadece bir veri kümesini temsil ediyorsa , bu konuda bilinçsiz bir şey yoktur; Persondeğişkenler gerçekten nesneleri değil, soyut değerleri temsil eder . (Bu durumda, muhtemelen yeniden adlandırmak daha uygun olacaktır PersonData.) Eğer Persongerçekten bir kişinin kendisini modelliyorsa, klonları sürekli olarak oluşturma ve taşıma fikri, değiştirdiğiniz düşünme tuzağından kaçınsanız bile saçmadır. orijinal. Bu durumda, sadece Personbir referans tipi (yani bir sınıf) yapmak daha doğal olurdu .

İşlevsel programlamanın bize öğrettiği gibi, her şeyi değişmez hale getirmenin faydaları vardır (hiç kimse gizlice ericona referansta bulunamaz ve onu değiştiremez), ancak bu OOP'ta deyimsel olmadığı için, hala sizinle çalışan başkaları için sezgisel olmayacaktır. kodu.


3
Kimlik hakkındaki görüşünüz iyi bir şey; kimliğin yalnızca bir şeye birden çok referans olduğunda geçerli olduğunu belirtmek gerekir. Eğer fooevrende hedef her yerde tek referansı tutan ve hiçbir şey nesnenin kimlik karma değer, daha sonra mutasyona alanda bu yakalamıştır foo.Xyapmaya anlama sahip olduğu foosadece daha önce değinilen bir daha benzer bir yeni nesneye noktası, fakat Xistenen değeri tutarak. Sınıf türleriyle, bir şeye birden fazla referans olup olmadığını bilmek genellikle zordur, ancak yapılarla kolaydır: yoktur.
supercat

1
Eğer Thingbir değişken sınıf türüdür bir Thing[] edecek nesne kimlikleri saklanması - kimse ya da olmamak isteyip istemediğini - Bir hayır sağlayabilirsiniz sürece Thingherhangi bir dış referanslar mevcut olduğu için dizide hiç mutasyona edilecektir. Dizi öğelerinin kimliğini kapsüllemesini istemiyorsa, genel olarak ya referanslarını tuttuğu hiçbir öğenin mutasyona uğramayacağından ya da sahip olduğu öğelere hiçbir dış referans kalmayacağından emin olunmalıdır [melez yaklaşımlar da işe yarayabilir ]. Her iki yaklaşım da son derece elverişlidir. Eğer Thingbir yapıdır, bir Thing[]kapsülleri yalnızca değer verir.
supercat

Nesneler için kimlikleri konumlarından gelir. Referans türlerinin örnekleri, bellekteki konumları sayesinde kimliğine sahiptir ve değerlerinin depolandıkları dış mekanda kimlikleri olmasına rağmen, yalnızca kimliklerini (bir referansı) iletir, verilerini değil. Eric değer türünüzün kimliği yalnızca depolandığı değişkenden gelir. Onu geçerseniz, kimliğini kaybeder.
IllidanS4 Monica'nın

6

Yapılarla (ve C # ile değil) bir ilgisi yoktur, ancak Java'da, örneğin bir karma haritadaki anahtarlar olduklarında değiştirilebilir nesnelerle ilgili sorunlar yaşayabilirsiniz. Haritaya ekledikten sonra bunları değiştirirseniz ve bu, karma kodunu değiştirirse , kötü şeyler olabilir.


3
Haritada sınıf olarak da bir sınıf kullanıyorsanız bu doğrudur.
Marc Gravell

6

Şahsen koda baktığımda aşağıdaki bana oldukça hantal görünüyor:

data.value.set (data.value.get () + 1);

basitçe değil

data.value ++; veya data.value = data.value + 1;

Veri kapsülleme, bir sınıfı aktarırken kullanışlıdır ve değerin kontrollü bir şekilde değiştirilmesini sağlamak istersiniz. Ancak, herkese açık kümeye sahip olduğunuzdan ve bu değerin geçirilen değere ayarlanmasından biraz daha fazlasını yapan işlevlere sahip olduğunuzda, bu sadece bir genel veri yapısının etrafından geçilmesinde nasıl bir gelişme olur?

Bir sınıf içinde özel bir yapı oluşturduğumda, bu yapıyı bir gruptaki değişkenleri düzenlemek için oluşturdum. Bu yapıyı sınıf kapsamı içinde değiştirmek, o yapının kopyalarını almak ve yeni örnekler oluşturmak istemiyorum.

Bana göre bu, genel değişkenleri düzenlemek için kullanılan yapıların geçerli kullanımını önler, erişim kontrolü istersem bir sınıf kullanırdım.


1
Noktasına doğru! Yapılar, erişim kontrolü kısıtlamaları olmayan organizasyon birimleridir! Ne yazık ki, C # onları bu amaç için işe yaramaz hale getirdi!
ThunderGr

her iki örnekte de değişken yapılar gösterildiğinden, bu tamamen noktayı kaçırır.
vidstige

C # onları bu amaç için işe yaramaz hale getirdi, çünkü bu yapıların amacı değil
Luiz Felipe

6

Bay Eric Lippert'in örneğiyle ilgili birkaç sorun var. Yapıların kopyalandığı noktayı ve dikkatli değilseniz bunun nasıl bir sorun olabileceğini göstermek için çaba sarf edilmektedir. Örneğe baktığımda, yapıyla ya da sınıfla ilgili bir sorun değil, kötü bir programlama alışkanlığı sonucu görüyorum.

  1. Bir yapının yalnızca kamu üyelerine sahip olduğu varsayılır ve herhangi bir kapsülleme gerektirmemelidir. Eğer öyleyse gerçekten bir tür / sınıf olmalıdır. Aynı şeyi söylemek için iki yapıya gerçekten ihtiyacınız yok.

  2. Bir yapıyı çevreleyen sınıfınız varsa, üye yapıyı değiştirmek için sınıfta bir yöntem çağırırsınız. İyi bir programlama alışkanlığı olarak yapacağım şey bu.

Uygun bir uygulama aşağıdaki gibi olacaktır.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

Yapı ile ilgili bir sorunun aksine, programlama alışkanlığı ile ilgili bir sorun gibi görünüyor. Yapıların değişebilir olması gerekiyor, fikir ve niyet bu.

Değişikliklerin sonucu beklendiği gibi davranıyor:

1 2 3 Devam etmek için herhangi bir tuşa basın. . .


4
Değişmez sınıf objeleri gibi davranacak küçük opak yapılar tasarlamakta yanlış bir şey yoktur; bir nesne gibi davranan bir şey yapmaya çalışırken MSDN yönergeleri makul olur . Yapılar, nesneler gibi davranan hafif şeylere ihtiyaç duyulan bazı durumlarda ve koli bandı ile birbirine yapışmış bir grup değişkene ihtiyaç duyduğu durumlarda uygundur. Bununla birlikte, bir nedenden ötürü, birçok insan yapıların iki ayrı kullanımı olduğunu ve birine uygun yönergelerin diğeri için uygun olmadığını fark edemez.
supercat

5

Değişken verilere göre birçok avantaj ve dezavantaj vardır. Milyon dolarlık dezavantaj takmadır. Aynı değer birden fazla yerde kullanılıyorsa ve bunlardan biri değeri değiştiriyorsa, onu kullanan diğer yerlere sihirli bir şekilde değişmiş gibi görünecektir. Bu, yarış koşullarıyla ilgilidir, ancak aynı değildir.

Milyon dolarlık avantaj bazen modülerliktir. Değişken durumu, değiştirilmeyi gerektirmeyen koddaki değişen bilgileri gizlemenize izin verebilir.

Tercüman Sanatı, bu değiş tokuşlara ayrıntılı olarak girer ve bazı örnekler verir.


c # 'da yapıların takma adı yoktur. Her yapı ataması bir kopyadır.
özyinelemeli

@recursive: Bazı durumlarda, bu değişebilir yapıların büyük bir avantajıdır ve bu da yapıların değişebilir olmaması gerektiği fikrini sorgulamamı sağlar. Derleyicilerin bazen örtük olarak yapıları kopyalaması, değişebilir yapıların kullanışlılığını azaltmaz.
supercat

1

Doğru kullanıldıklarında kötü olduklarına inanmıyorum. Üretim koduma koymazdım, ancak bir yapının ömrünün nispeten küçük olduğu yapılandırılmış birim test alayları gibi bir şey için yapardım.

Eric örneğini kullanarak, belki de bu Eric'in ikinci bir örneğini oluşturmak istersiniz, ancak ayarlamalar yapın, çünkü testinizin doğası budur (yani çoğaltma, sonra değiştirme). Test komut dosyasının geri kalanında sadece Eric2 kullanıyorsanız, onu test karşılaştırması olarak kullanmayı düşünmüyorsanız, Eric'in ilk örneğinde ne olduğu önemli değildir.

Bu çoğunlukla, sığ olan belirli bir nesneyi (yapıların noktası) tanımlayan eski kodu test etmek veya değiştirmek için yararlı olacaktır, ancak değişmez bir yapıya sahip olarak, bu durumun can sıkıcı bir şekilde kullanılmasını önler.


Gördüğüm gibi, bir yapı, koli bandı ile birbirine yapışmış bir grup değişkenin kalbinde. Bir yapının koli bandı ile birbirine yapışmış bir grup değişken dışında bir şey gibi davranması .NET için mümkündür ve pratik olduğunda bir grup değişkenin birbirine yapışmış gibi davranacak bir türün koli bandı ile birleşik bir nesne gibi davranmalıdır (bir yapı için değişmezlik anlamına gelir), ancak bazen koli bandı ile birlikte bir grup değişkeni yapıştırmak yararlı olur. Üretim kodunda bile, bir tipe sahip olmayı daha iyi
düşünürdüm

... "her alan kendisine yazılan son şeyi içerir" ifadesinin ötesinde hiçbir anlambilim içermeyen, tüm anlambilimi , yapıyı kullanan koda iterek , bir yapının daha fazlasını yapmasına çalışmaktan daha fazla. Örneğin, Range<T>üyeleri Minimumve tür Maximumalanları Tve kodları olan bir tür göz önüne alındığında, Range<double> myRange = foo.getRange();neyin Minimumve Maximumiçerdiği hakkında herhangi bir garanti gelmelidir foo.GetRange();. Having Rangeaçıktaki alan yapı kendine ait herhangi bir davranış eklemek için gitmiyor açık olduğunu yapacak olması.
supercat
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.