Sınıf yerine bir yapı ne zaman kullanmalıyım?


302

MSDN, hafif nesnelere ihtiyacınız olduğunda yapıları kullanmanız gerektiğini söylüyor. Bir yapı bir sınıfa tercih edildiğinde başka senaryolar var mı?

Bazı insanlar bunu unutmuş olabilir:

  1. yapıların yöntemleri olabilir.
  2. yapılar miras alınamaz.

Yapılar ve sınıflar arasındaki teknik farklılıkları anlıyorum, bir yapının ne zaman kullanılacağı konusunda iyi bir fikrim yok .


Sadece bir hatırlatma - çoğu insan bu bağlamda unutmak eğilimindedir C # yapıları da yöntemler olabilir olmasıdır.
petr k.

Yanıtlar:


295

MSDN şu cevaba sahiptir: Sınıflar ve Yapılar Arasında Seçim Yapma .

Temel olarak, bu sayfa size 4 öğeli bir kontrol listesi verir ve türünüz tüm ölçütleri karşılamadığı sürece bir sınıf kullandığını söyler.

Tür aşağıdaki özelliklerin hepsine sahip olmadığı sürece bir yapı tanımlamayın:

  • Mantıksal olarak ilkel tiplere (tamsayı, çift vb.) Benzer tek bir değeri temsil eder.
  • 16 bayttan küçük bir örnek boyutuna sahiptir.
  • Değişmez.
  • Sık sık kutsanması gerekmeyecek.

1
Belki bariz bir şeyi kaçırıyorum ama "değişmez" kısmın ardındaki mantığı tam olarak anlamıyorum. Bu neden gerekli? Birisi açıklayabilir mi?
Tamas Czinege

3
Muhtemelen bunu tavsiye ettiler çünkü yapı değişmezse, referans anlambiliminden ziyade değer semantiğine sahip olması önemli olmayacaktır. Ayrım yalnızca nesneyi / yapıyı bir kopyasını yaptıktan sonra değiştirirseniz önemlidir.
Stephen C. Steel

@DrJokepu: Bazı durumlarda, sistem bir yapının geçici bir kopyasını oluşturur ve ardından bu kopyanın onu değiştiren koda referans olarak iletilmesine izin verir; geçici nüsha atılacağı için değişiklik kaybolacak. Bir yapı onu mutasyona uğratan yöntemlere sahipse bu sorun özellikle ciddidir. Değişkenliğin bir şey yapmak için bir neden olduğu fikrine kesinlikle katılmıyorum, çünkü - c # ve vb.net'teki bazı eksikliklere rağmen, değişebilir yapılar başka hiçbir şekilde elde edilemeyen yararlı anlamlar sağlar; bir sınıfa değişmez bir yapı tercih etmek için semantik bir neden yoktur.
supercat

4
@Chuu: JIT derleyicisini tasarlarken, Microsoft 16 bayt veya daha küçük yapıları kopyalamak için kodu optimize etmeye karar verdi; bu, 17 baytlık bir yapıyı kopyalamanın, 16 baytlık bir yapıyı kopyalamaktan çok daha yavaş olabileceği anlamına gelir. Microsoft'un bu tür optimizasyonları daha büyük yapılara genişletmesini beklemek için özel bir neden görmüyorum, ancak 17 baytlık yapıların kopyalanması 16 baytlık yapılardan daha yavaş olsa da, büyük yapıların daha verimli olabileceği birçok durum var. büyük sınıf nesneler ve yapıların göreceli avantajının yapı boyutu ile büyüdüğü yerlerde .
supercat

3
@Chuu: Sınıflara yapacağı gibi aynı yapıları büyük yapılara uygulamak verimsiz bir kodla sonuçlanmaya meyillidir, ancak uygun çözüm genellikle yapıları sınıflarla değiştirmek yerine yapıları daha verimli kullanmaktır; en önemlisi, yapıları değere göre iletmekten veya döndürmekten kaçınmalıdır. refMakul olduğunda bunları parametre olarak iletin. 4.000 alanlı bir yapıyı, değiştirilmiş sürümü döndüren bir yönteme değerine göre 4 alanlı bir yapıyı geçirmekten daha ucuz olacak bir yönteme ref parametresi olarak geçirin.
supercat

53

En önemli yönü düşündüğüm önceki cevabın hiçbirini okumadığım için şaşırdım:

Kimliği olmayan bir tür istediğimde yapıları kullanıyorum. Örneğin bir 3B nokta:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Bu yapının iki örneği varsa, bunların bellekte tek bir veri parçası mı yoksa iki tane mi olduğunu umursamazsınız. Sadece sahip oldukları değerlere önem veriyorsunuz.


4
Konu biraz bit, ama obj ThreeDimensionalPoint olmadığında neden bir ArgumentException veriyorsunuz? Bu durumda sadece yanlış döndürmez misin?
Svish

4
Bu doğru, aşırı istekliydim. return falseorada olması gereken şey, şimdi düzeltmek.
Andrei Rînea

Bir yapı kullanmak için ilginç bir neden. GetHashCode ve burada göstermek ne benzer tanımlanmış Eşit sınıfları yaptık, ama sonra her zaman sözlük anahtarları olarak kullandığınız takdirde bu örnekleri mutasyona değil dikkatli olmak zorunda kaldı. Onları yapı olarak tanımlasaydım muhtemelen daha güvenli olurdu. (O zaman anahtar , yapı bir sözlük anahtarı haline geldiğinde alanların bir kopyası olacaktır , bu yüzden daha sonra orijinali değiştirirsem anahtar değişmeden kalacaktır.)
ToolmakerSteve

Örneğinizde sorun yok çünkü sadece 12 baytınız var, ancak bu yapıda 16 baytı aşan çok fazla alanınız varsa, bir sınıf kullanmayı ve GetHashCode ve Equals yöntemlerini geçersiz kılmayı düşünmeniz gerektiğini unutmayın.
Daniel Botero Correa

DDD'deki bir değer türü mutlaka C # 'da bir değer türü kullanmanız gerektiği anlamına gelmez
Darragh

26

Bill Wagner'in "etkili c #" kitabında bununla ilgili bir bölüm var ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Aşağıdaki prensibi kullanarak sonuçlandırır:

  1. Veri depolama türünün ana sorumluluğu var mı?
  2. Genel arayüzü tamamen veri üyelerine erişen veya bunları değiştiren özellikler tarafından tanımlanıyor mu?
  3. Türünüzün asla alt sınıfları olmayacağından emin misiniz?
  4. Tipinizin asla polimorfik olarak muamele görmeyeceğinden emin misiniz?

4 sorunun hepsine 'evet' cevabını verirseniz: bir yapı kullanın. Aksi takdirde bir sınıf kullanın.


1
yani ... veri aktarım nesneleri (DTO) yapı olmalı?
Cruizer

1
Yukarıda açıklanan 4 kriteri karşılıyorsa evet derim. Bir veri aktarım nesnesinin neden belirli bir şekilde ele alınması gerekir?
Bart Gijssens

2
@cruizer sizin durumunuza bağlıdır. Bir projede, DTO'larımızda ortak denetim alanlarımız vardı ve bu nedenle diğerlerinin miras aldığı bir temel DTO yazdık.
Andrew Grothe

1
(2) hariç hepsi mükemmel ilkeler gibi görünüyor. (2) ile tam olarak ne demek istediğini ve nedenini bilmek için muhakemesini görmesi gerekirdi.
ToolmakerSteve

1
@ToolmakerSteve: Bunun için kitabı okumanız gerekecek. Bir kitabın büyük bölümlerini kopyalamanın / yapıştırmanın adil olduğunu düşünmeyin.
Bart Gijssens


12

Ben ne zaman yapılar kullanırsınız:

  1. bir nesnenin salt okunur olması gerekir (bir yapıyı her geçtiğinizde / atadığınızda kopyalanır). Çoğu durumda kilitleme gerektirmediğinden, çok iş parçacıklı işleme söz konusu olduğunda salt okunur nesneler harikadır.

  2. bir nesne küçük ve kısa ömürlüdür. Böyle bir durumda, nesnenin yığına tahsis edilmesi şansı yüksektir, bu da yönetilen yığın üzerine koymaktan çok daha verimlidir. Dahası, nesne tarafından ayrılan bellek, kapsamının dışına çıkar çıkmaz boşaltılır. Başka bir deyişle, Çöp Toplayıcı için daha az iştir ve bellek daha verimli kullanılır.


10

Aşağıdaki durumlarda bir sınıf kullanın:

  • Kimliği önemlidir. Yapılar, değere göre bir yönteme geçirildiğinde örtük olarak kopyalanır.
  • Geniş bir bellek alanına sahip olacak.
  • Alanları başlangıç ​​değerlerine ihtiyaç duyar.
  • Bir temel sınıftan miras almanız gerekir.
  • Polimorfik davranışa ihtiyacınız var;

Aşağıdaki durumlarda bir yapı kullanın:

  • İlkel bir tip gibi davranacaktır (int, uzun, bayt, vb.).
  • Küçük bir bellek alanına sahip olması gerekir.
  • Bir yapının değere göre aktarılmasını gerektiren bir P / Invoke yöntemini çağırıyorsunuz.
  • Çöp toplamanın uygulama performansı üzerindeki etkisini azaltmanız gerekir.
  • Alanlarının yalnızca varsayılan değerlerine sıfırlanması gerekir. Bu değer, sayısal türler için sıfır, Boolean türler için yanlış ve referans türleri için null olur.
    • C # 6.0 yapılarında, yapı alanlarının varsayılan olmayan değerlere başlatılmasında kullanılabilecek varsayılan bir yapıcı olabileceğini unutmayın.
  • Bir temel sınıftan (tüm yapıların devraldığı ValueType dışında) miras almanıza gerek yoktur.
  • Polimorfik davranışa ihtiyacınız yoktur.

5

Her zaman bir yöntem çağrısından bir şeyler geri aktarmak için birkaç değeri bir araya getirmek istediğimde bir yapı kullandım, ancak bu değerleri okuduktan sonra herhangi bir şey için kullanmam gerekmeyecek. Tıpkı işleri temiz tutmanın bir yolu gibi. Bir yapıdaki şeyleri "fırlatma" ve sınıftaki şeyleri daha kullanışlı ve "işlevsel" olarak görme eğilimindeyim


4

Eğer bir varlık değişmez olacaksa, bir yapı mı yoksa sınıf mı kullanılacağı sorusu anlambilimden ziyade genellikle performanstan biri olacaktır. 32/64-bit bir sistemde, sınıf referansları, sınıftaki bilgi miktarından bağımsız olarak depolanması için 4/8 bayt gerektirir; sınıf referansını kopyalamak 4/8 bayt kopyalamayı gerektirir. Öte yandan, her farklısınıf örneği, sahip olduğu bilgilere ve referanslara ilişkin bellek maliyetine ek olarak 8/16 bayt ek yüke sahip olacaktır. Her birinin dört adet 32 ​​bitlik tam sayıya sahip 500 varlıklık bir dizi istediğini varsayalım. Varlık bir yapı türüyse, dizi, 500 varlığın tümü aynı, hepsi farklı veya aralarında bir yer olup olmadığına bakılmaksızın 8.000 bayt gerektirir. Varlık bir sınıf türüyse, 500 başvuru dizisi 4.000 bayt alır. Bunların tümü farklı nesnelere işaret ediyorsa, nesnelerin her biri ek 24 bayt (500'ün tamamı için 12.000 bayt), toplam 16.000 bayt - bir yapı türünün depolama maliyetinin iki katı gerekir. Öte yandan, kod bir nesne örneği oluşturdu ve daha sonra 500 dizi yuvasına bir referans kopyaladı, toplam maliyet o örnek için 24 bayt ve 4, Dizi için 000 - toplam 4.024 bayt. Büyük bir tasarruf. Sonuncunun yanı sıra çok az sayıda durum işe yarayacaktır, ancak bazı durumlarda bu tür paylaşımları değerli kılmak için yeterli sayıda dizi yuvasına bazı referanslar kopyalamak mümkün olabilir.

Varlığın değişebilir olması gerekiyorsa, bir sınıf veya yapı kullanılıp kullanılmayacağı sorusu bazı açılardan daha kolaydır. "Şey" in, x adında bir tamsayı alanına sahip bir yapı veya sınıf olduğunu ve birinin aşağıdaki kodu yaptığını varsayalım:

  Şey tl, t2;
  ...
  t2 = tl;
  t2.x = 5;

İkinci ifade t1.x'i etkilemek ister mi?

Şey bir sınıf tipiyse, t1 ve t2 eşdeğer olacaktır, yani t1.x ve t2.x de eşdeğer olacaktır. Böylece, ikinci ifade t1.x'i etkileyecektir. Şey bir yapı tipiyse, t1 ve t2 farklı örnekler olacaktır, yani t1.x ve t2.x farklı tamsayıları ifade edecektir. Dolayısıyla, ikinci ifade t1.x'i etkilemeyecektir.

Değişken yapılar ve değişebilir sınıflar temelde farklı davranışlara sahiptir, ancak .net'in yapısal mutasyonları ele almasında bazı tuhaflıkları vardır. Kişi değer türü davranışlar istiyorsa ("t2 = t1" ifadesinin, t1 ve t2'yi ayrı örnek olarak bırakırken verileri t1'den t2'ye kopyalayacağı anlamına gelir) ve .net'in değer türlerini işleyişindeki tuhaflıklarla yaşayabiliyorsa, yapı. Birisi değer türü anlambilim istiyor, ancak .net'in tuhaflıkları kişinin uygulamasında değer türü anlambilimine yol açacaksa, bir sınıf ve mumble kullanın.


3

Buna ek olarak, yukarıdaki mükemmel cevaplar:

Yapılar değer tipleridir.

Hiçbir zaman Hiçbir şey olarak ayarlanamazlar .

Bir yapı ayarlama = Hiçbir şey, tüm değer türlerini varsayılan değerlerine ayarlayamaz.


2

gerçekten davranışa ihtiyacınız olmadığında, ancak basit bir dizi veya sözlükten daha fazla yapıya ihtiyacınız olduğunda.

Takip Genel olarak yapıları böyle düşünüyorum. Yöntemleri olabileceğini biliyorum, ama bu zihinsel ayrımı tutmayı seviyorum.


Neden öyle diyorsun? Yapıların yöntemleri olabilir.
Esteban Araya

2

@Simon'un dediği gibi, yapılar "değer türü" anlambilim sağlar, bu nedenle yerleşik bir veri türüne benzer bir davranışa ihtiyacınız varsa bir yapı kullanın. Yapılar kopya ile geçtiğinden, küçük boyutlarda, yaklaşık 16 bayt olduğundan emin olmak istersiniz.


2

Bu eski bir konudur, ancak basit bir kıyaslama testi sağlamak istemiştir.

İki .cs dosyası oluşturdum:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

ve

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Kıyaslama testi:

  • 1 TestClass oluştur
  • 1 TestStruct oluştur
  • 100 TestClass oluştur
  • 100 TestStruct oluştur
  • 10000 TestClass oluştur
  • 10000 TestStruct oluştur

Sonuçlar:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |

1

Hmm ...

Çöp toplamayı, yapıların sınıflara karşı kullanımına karşı / argüman olarak kullanmam. Yönetilen yığın bir yığın gibi çalışır - bir nesne oluşturmak onu yığının en üstüne koyar, bu da yığın üzerine tahsis etmek kadar hızlıdır. Ek olarak, bir nesne kısa ömürlüdür ve bir GC döngüsünde hayatta kalmazsa, GC yalnızca hala erişilebilir olan bellekle çalıştığından, serbest bırakma serbesttir. (Arama MSDN, .NET bellek yönetimi hakkında bir dizi makale var, onlar için kazmak için çok tembelim).

Çoğu zaman bir yapı kullanıyorum, bunu yapmak için kendimi tekmeliyorum, çünkü daha sonra referans anlambilimine sahip olmanın işleri biraz daha basit hale getireceğini keşfediyorum.

Her neyse, yukarıda yayınlanan MSDN makalesindeki bu dört nokta iyi bir rehber gibi görünüyor.


1
Bazen bir yapıya sahip referans semantiğe ihtiyacınız varsa, basitçe beyan edin class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }ve sonra bir MutableHolder<T>değişebilir sınıf semantiğine sahip bir nesne olacaktır (bu Tbir yapı veya değişmez sınıf türü ise de işe yarar ).
supercat

1

Yapılar Yığın üzerinde değil, bu nedenle iş parçacığı açısından güvenlidir ve aktarım nesnesi desenini uygularken kullanılmalıdır, Yığın üzerinde uçucu oldukları nesneleri asla kullanmak istemezsiniz, bu durumda Çağrı Yığını'nı kullanmak istersiniz, buradaki cevapların çıkışından şaşırdığım bir yapı kullanmak için temel bir durum,


-3

Bence en iyi cevap sadece özellikleri ve davranışları bir koleksiyon olduğunda özellikleri bir koleksiyon, sınıf bir yapı zaman yapılandırmak kullanmaktır.


yapılar da yöntemler olabilir
Nithin Chandran

Tabii ki, ancak yöntemlere ihtiyacınız varsa% 99 olasılıkla sınıf yerine struct kullanıyorsunuz demektir. Struct içinde yöntemlere sahip olmanın uygun olmadığını tespit ettiğim tek istisnalar geri aramalardır
Lucian Gabriel Popescu
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.