Yapıların arayüzleri uygulaması güvenli midir?


94

Yapılar için CLR'de arayüzleri C # aracılığıyla uygulamasının ne kadar kötü olduğu hakkında bir şeyler okuduğumu hatırlıyorum, ancak bu konuda hiçbir şey bulamıyorum. Kötü bir şey mi? Bunu yapmanın istenmeyen sonuçları var mı?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }

Yanıtlar:


45

Bu soruda devam eden birkaç şey var ...

Bir yapının bir arayüz uygulaması mümkündür, ancak döküm, değişebilirlik ve performansla ilgili endişeler vardır. Daha fazla ayrıntı için bu gönderiye bakın: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

Genel olarak yapılar, değer tipi anlambilimine sahip nesneler için kullanılmalıdır. Bir yapı üzerinde bir arabirim uygulayarak, yapı yapı ve arabirim arasında ileri geri atılırken, kutu sorunlarıyla karşılaşabilirsiniz. Kutulama sonucunda yapının iç durumunu değiştiren işlemler düzgün çalışmayabilir.


3
"Kutunun bir sonucu olarak, yapının iç durumunu değiştiren işlemler düzgün çalışmayabilir." Bir örnek verin ve cevabı alın.

2
@Will: Yorumunuzda neyi kastettiğinizden emin değilim. Referans verdiğim blog gönderisinde, yapı üzerinde bir arayüz yönteminin çağrılmasının dahili değeri gerçekten değiştirmediğini gösteren bir örnek var.
Scott Dorman

12
@ScottDorman: Bazı durumlarda, arayüzler uygulayan yapılara sahip olmak kutudan kaçınmaya yardımcı olabilir . Başlıca örnekler IComparable<T>ve IEquatable<T>. Bir yapının Foobir tür değişkeninde depolanması IComparable<Foo>kutulama gerektirir, ancak bir genel tip biriyle Tsınırlandırılırsa, ikisini de kutuya almak zorunda kalmadan ve kısıtlamayı uygulamaktan başka bir şey bilmek zorunda kalmadan IComparable<T>onu diğeriyle karşılaştırabilir . Bu tür avantajlı davranış, yalnızca yapının arayüzleri uygulama becerisiyle mümkün olur. Bu söylendi ...TT
supercat

3
... bir sınıf nesnesinin veya kutulu yapının istenen değerlere sahip olmasının mümkün olmayacağı bazı bağlamlar olduğundan, belirli bir arayüzün yalnızca kutusuz yapılara uygulanabilir olarak kabul edilmesi gerektiğini bildirmenin bir yolu olsaydı güzel olabilirdi. davranışlar.
supercat

2
"yapılar, değer türü semantiği olan nesneler için kullanılmalıdır. Yapının iç durumunu değiştiren işlemler düzgün çalışmayabilir." Buradaki gerçek sorun, değer türü anlambilim ile değişkenliğin iyi karışmaması değil mi?
jpmc26

185

Başka hiç kimse bu cevabı açıkça vermediği için aşağıdakileri ekleyeceğim:

Bir yapı üzerine bir arayüz uygulamanın hiçbir olumsuz sonucu yoktur.

Bir yapıyı tutmak için kullanılan arabirim türünün herhangi bir değişkeni , kullanılan yapının kutulu bir değeriyle sonuçlanacaktır. Yapı değişmez ise (iyi bir şey), o zaman bu en kötü ihtimalle bir performans sorunudur:

  • ortaya çıkan nesneyi kilitleme amacıyla kullanmak (herhangi bir şekilde son derece kötü bir fikir)
  • referans eşitliği semantiğini kullanarak ve aynı yapıdan iki kutulu değer için çalışmasını bekleyerek.

Bunların her ikisi de olası değildir, bunun yerine aşağıdakilerden birini yapmanız olasıdır:

Jenerikler

Arayüzleri uygulayan yapıların belki de birçok makul nedeni , kısıtlamalarla genel bir bağlam içinde kullanılabilmeleridir . Bu şekilde kullanıldığında değişken şöyle:

class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
    private readonly T a;

    public bool Equals(Foo<T> other)
    {
         return this.a.Equals(other.a);
    }
}
  1. Yapının bir tür parametresi olarak kullanımını etkinleştirin
    • new()veya benzeri başka bir kısıt classkullanılmadığı sürece .
  2. Bu şekilde kullanılan yapılarda kutudan kaçınılmasına izin verin.

O zaman this.a bir arayüz referansı DEĞİLDİR, bu yüzden içine yerleştirilen şeyin bir kutusuna neden olmaz. Ayrıca, c # derleyici genel sınıfları derlediğinde ve Type parametresi T'nin örneklerinde tanımlanan örnek yöntemlerinin çağrılarını eklemesi gerektiğinde, kısıtlanmış işlem kodunu kullanabilir :

ThisType bir değer türü ise ve thisType yöntemi uygularsa, ptr, thisType tarafından yöntemin uygulanması için bir çağrı yöntemi talimatına 'this' işaretçisi olarak değiştirilmemiş olarak iletilir.

Bu, kutulamayı önler ve değer türü arabirim uyguladığından, yöntemi uygulamak zorundadır , böylece kutulama meydana gelmez. Yukarıdaki örnekte Equals()çağrı, bunun üzerinde kutu olmadan yapılır. A 1 .

Düşük sürtünmeli API'ler

Çoğu yapı, bitsel özdeş değerlerin eşit 2 olarak kabul edildiği ilkel benzeri semantiğe sahip olmalıdır . Çalışma zamanı, bu tür davranışları örtük olarak sağlayacaktır, Equals()ancak bu yavaş olabilir. Ayrıca, bu örtülü eşitlik edilir değil bir uygulama olarak ortaya IEquatable<T>ve böylece açıkça kendileri uygulamadığınız sürece yapılar Sözlükler tuşları olarak kolayca kullanılıyor önler. Bu nedenle, birçok genel yapı türünün uyguladıklarını bildirmesi yaygındır IEquatable<T>(neredeT bunu daha kolay ve daha iyi performans göstermesinin yanı sıra CLR BCL içindeki birçok mevcut değer türünün davranışıyla tutarlı hale getirmek için kendileri .

BCL'deki tüm ilkeller minimumda uygular:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T>(Ve böylece IEquatable)

Birçoğu da uygular IFormattable , DateTime, TimeSpan ve Guid gibi Sistem tarafından tanımlanan değer türlerinin birçoğunu da uygular, bunların birçoğunu veya tamamını da uygular. Karmaşık sayı yapısı veya bazı sabit genişlikli metin değerleri gibi benzer şekilde 'yaygın olarak kullanışlı' bir tür uyguluyorsanız, bu ortak arabirimlerin çoğunu (doğru şekilde) uygulamak, yapınızı daha kullanışlı ve kullanışlı hale getirecektir.

İstisnalar

Açıkçası, eğer arayüz güçlü bir şekilde değişkenliği (örneğin ICollection) ima ediyorsa, o zaman bunu uygulamak kötü bir fikirdir, çünkü yapıyı değiştirilebilir hale getirmişsinizdir (değişikliklerin orijinal yerine kutulu değerde meydana geldiği halihazırda açıklanan türden hatalara yol açar. ) veya gibi yöntemlerin sonuçlarını görmezden gelerek Add()veya istisnalar atarak kullanıcıların kafasını karıştırırsınız .

Çoğu arayüz, değişkenliği (gibi IFormattable) ifade ETMEZ ve belirli işlevselliği tutarlı bir şekilde ortaya çıkarmanın deyimsel bir yolu olarak hizmet eder. Genellikle yapının kullanıcısı, bu tür bir davranış için herhangi bir boks ek yükünü önemsemeyecektir.

Özet

Makul bir şekilde, değişmez değer türlerinde yapıldığında, kullanışlı arayüzlerin uygulanması iyi bir fikirdir


Notlar:

1: Derleyicinin bunu, belirli bir yapı türünde olduğu bilinen , ancak bir sanal yöntemi çağırması gereken değişkenler üzerinde sanal yöntemleri çağırırken kullanabileceğini unutmayın . Örneğin:

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

List tarafından döndürülen numaralandırıcı, listeyi numaralandırırken ayırmayı önlemek için bir optimizasyon olan bir yapıdır (Bazı ilginç sonuçlarla ). Ancak foreach semantik listeleyicisi aletlerin eğer belirtmek IDisposablesonraDispose() yineleme tamamlandıktan çağrılacağını . Açıkçası, bunun kutulu bir çağrı yoluyla gerçekleşmesi, numaralandırıcının bir yapı olmasının herhangi bir faydasını ortadan kaldıracaktır (aslında daha kötü olurdu). Daha da kötüsü, eğer dispose çağrısı numaralandırıcının durumunu bir şekilde değiştirirse, bu durum kutulu örnekte olur ve karmaşık durumlarda pek çok ince hata ortaya çıkabilir. Bu nedenle, bu tür bir durumda yayılan IL:

IL_0001: newobj System.Collections.Generic.List..ctor
IL_0006: stloc.0     
IL_0007: hayır         
IL_0008: ldloc.0     
IL_0009: callvirt System.Collections.Generic.List.GetEnumerator
IL_000E: stloc.2     
IL_000F: br.s IL_0019
IL_0011: ldloca.s 02 
IL_0013: System.Collections.Generic.List.get_Current çağrısı
IL_0018: stloc.1     
IL_0019: ldloca.s 02 
IL_001B: System.Collections.Generic.List.MoveNext çağrısı
IL_0020: stloc.3     
IL_0021: ldloc.3     
IL_0022: brtrue.s IL_0011
IL_0024: ayrılın. S IL_0035
IL_0026: ldloca.s 02 
IL_0028: kısıtlı. System.Collections.Generic.List.Enumerator
IL_002E: callvirt System.IDisposable.Dispose
IL_0033: hayır         
IL_0034: son olarak  

Dolayısıyla, IDisposable'ın uygulanması herhangi bir performans sorununa neden olmaz ve Dispose yöntemi gerçekten bir şey yaparsa numaralandırıcının (üzücü) değiştirilebilir yönü korunur!

2: double ve float, NaN değerlerinin eşit kabul edilmediği bu kuralın istisnalarıdır.


1
Egheadcafe.com sitesi taşındı, ancak içeriğini koruyarak iyi bir iş çıkaramadı. Denedim, ancak OP bilgisi olmadan eggheadcafe.com/software/aspnet/31702392/… ' nin orijinal belgesini bulamadım . (Mükemmel bir özet için PS +1).
Abel

2
Bu harika bir cevap, ancak "Özet" i "TL; DR" olarak en üste taşıyarak iyileştirebileceğinizi düşünüyorum. Önce sonucu vermek, okuyucunun işlerin nereye gittiğini bilmesine yardımcı olur.
Hans

Bir yayın yaparken derleyici uyarısı olmamalıdır structbir etmek interface.
Jalal

8

Bazı durumlarda, bir yapının bir arabirim uygulaması iyi olabilir (eğer hiç yararlı olmasaydı, .net'in yaratıcılarının bunu sağlayacağı şüphelidir). Bir yapı türü salt okunur bir arabirim uygularsa IEquatable<T>, yapıyı bir tür depolama konumunda (değişken, parametre, dizi öğesi, vb.) Depolamak, IEquatable<T>kutuya alınmasını gerektirir (her yapı türü aslında iki tür şeyi tanımlar: bir depolama bir değer türü olarak davranan konum türü ve bir sınıf türü olarak davranan bir yığın nesne türü; birincisi dolaylı olarak ikinciye dönüştürülebilir - "kutulama" - ve ikincisi, açık dönüştürme yoluyla birinciye dönüştürülebilir. "kutudan çıkarma"). Bir yapının arayüz uygulamasından kutulamadan yararlanmak mümkündür, ancak kısıtlı jenerikler denen şeyi kullanarak.

Örneğin, birinin bir yöntemi varsa CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, böyle bir yöntem thing1.Compare(thing2), kutuya thing1veya thing2. Eğer thing1, örneğin olur, bir Int32, çalışma zamanı onun için kod oluşturur zaman bilecek CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Hem yöntemi barındıran şeyin hem de parametre olarak iletilen şeyin tam türünü bileceğinden, ikisini de kutuya koymak zorunda kalmaz.

Arabirimleri uygulayan yapılarla ilgili en büyük sorun, arabirim türünde bir konumda depolanan bir yapının Objectveya ValueType(kendi türündeki bir konumun aksine) bir sınıf nesnesi gibi davranmasıdır. Salt okunur arabirimler için bu genellikle bir sorun değildir, ancak IEnumerator<T>bunun gibi bir değişime uğramış arabirim için bazı garip anlambilimlere yol açabilir.

Örneğin, aşağıdaki kodu düşünün:

List<String> myList = [list containing a bunch of strings]
var enumerator1 = myList.GetEnumerator();  // Struct of type List<String>.IEnumerator
enumerator1.MoveNext(); // 1
var enumerator2 = enumerator1;
enumerator2.MoveNext(); // 2
IEnumerator<string> enumerator3 = enumerator2;
enumerator3.MoveNext(); // 3
IEnumerator<string> enumerator4 = enumerator3;
enumerator4.MoveNext(); // 4

İşaretli ifade # 1 enumerator1, ilk öğeyi okumak için asal olacaktır . Bu numaralandırıcının durumu konumuna kopyalanacak enumerator2. İşaretli ifade # 2, bu kopyayı ikinci öğeyi okumak için ilerletir, ancak etkilemeyecektir enumerator1. Bu ikinci numaralayıcının durumu daha sonra kopyalanacak ve enumerator3işaretli ifade # 3 ile ilerletilecektir. Çünkü Ardından, enumerator3ve enumerator4her ikisi de referans tipleri, bir olan REFERANS için enumerator3daha sonra kopyalanacak enumerator4, böylece işaretlenmiş açıklamada etkili bir ilerleyecek hem enumerator3 ve enumerator4.

Bazı insanlar değer türlerinin ve referans türlerinin her iki tür olduğunu iddia etmeye çalışır Object, ancak bu gerçekten doğru değildir. Gerçek değer türleri dönüştürülebilir Object, ancak bunun örnekleri değildir. Bu List<String>.Enumeratortürün bir konumunda saklanan bir örneği , bir değer türü olup bir değer türü olarak davranır; türün bir konuma kopyalanması, onu IEnumerator<String>bir referans türüne dönüştürür ve bir başvuru türü olarak davranır . İkincisi bir çeşittir Object, ancak ilki değildir.

BTW, birkaç not daha: (1) Genel olarak, değişken sınıf türlerinin Equalsyöntemlerinin referans eşitliğini test etmesi gerekir , ancak kutulu bir yapının bunu yapmasının uygun bir yolu yoktur; (2) adına rağmen, ValueTypebir değer türü değil, bir sınıf türüdür; türetilen System.Enumtüm türler ValueType, hariç olmak üzere türetilen tüm türler gibi değer türleridir System.Enum, ancak her ikisi de ValueTypeve System.Enumsınıf türleridir.


3

Yapılar, değer türleri olarak uygulanır ve sınıflar başvuru türleridir. Foo türünde bir değişkeniniz varsa ve içinde bir Fubar örneğini depoluyorsanız, bunu bir referans türüne "kutuya koyar", böylece ilk etapta yapı kullanma avantajını ortadan kaldırır.

Bir sınıf yerine yapı kullanmayı görmemin tek nedeni, bir değer türü olması ve bir başvuru türü olmaması, ancak yapı bir sınıftan miras alamamasıdır. Yapıya sahipseniz bir arabirimi devralırsanız ve arabirimleri iletirseniz, yapının bu değer türü niteliğini kaybedersiniz. Arayüzlere ihtiyacınız varsa onu bir sınıf haline getirmeniz iyi olur.


Arayüzleri uygulayan ilkeller için de böyle mi çalışıyor?
aoetalks

3

(Ekleyecek önemli bir şey yok ama henüz düzenleme becerisine sahip değilsiniz, işte burada ..)
Mükemmel Güvenli. Yapılar üzerinde arabirim uygulamasında yasadışı bir şey yok. Ancak bunu neden yapmak istediğinizi sorgulamalısınız.

Bununla birlikte , bir yapıya bir arayüz referansı elde etmek onu KUTU altına alacaktır . Yani performans cezası vb.

Şu anda aklıma gelen tek geçerli senaryo buradaki yazımda gösteriliyor . Bir koleksiyonda depolanan bir yapının durumunu değiştirmek istediğinizde, bunu yapıda açığa çıkan ek bir arabirim aracılığıyla yapmanız gerekir.


Biri, Int32genel bir türü T:IComparable<Int32>(yöntemin genel bir tür parametresi veya yöntemin sınıfı olabilir) kabul eden bir yönteme geçerse , bu yöntem Compare, iletilen nesnede yöntemi kutulamadan kullanabilir.
supercat


0

Bir arayüz uygulayan bir yapının hiçbir sonucu yoktur. Örneğin, yerleşik sistem yapıları IComparableve gibi arayüzler uygular IFormattable.


0

Bir değer türünün bir arabirimi uygulaması için çok az neden vardır. Bir değer türünü alt sınıflandıramayacağınız için, ona her zaman somut türü olarak başvurabilirsiniz.

Elbette, hepsi aynı arayüzü uygulayan birden fazla yapınız olmadığı sürece, o zaman marjinal olarak yararlı olabilir, ancak bu noktada bir sınıfı kullanmanızı ve bunu doğru şekilde yapmanızı öneririm.

Tabii ki, bir arayüz uygulayarak, yapıyı kutuluyorsunuz, bu yüzden artık yığının üzerine oturuyor ve artık onu değere göre geçiremeyeceksiniz ... Bu, sadece bir sınıf kullanmanız gerektiğine dair fikrimi gerçekten güçlendiriyor bu durumda.


Somut uygulama yerine IComparable'ı ne sıklıkla geçiriyorsunuz?
FlySwat

IComparableDeğeri kutulamak için etrafta dolaşmanıza gerek yok . Basitçe IComparable, onu uygulayan bir değer türü ile bekleyen bir yöntemi çağırarak , değer türünü örtük olarak kutuya koyarsınız.
Andrew Hare

1
@AndrewHare: Kısıtlı jenerikler, yöntemlerin kutulamadan IComparable<T>tür yapıları üzerinde çağrılmasına izin verir T.
supercat

-10

Yapılar, yığında yaşayan sınıflar gibidir. "Güvensiz" olmaları için hiçbir neden göremiyorum.


Mirastan yoksun olmaları dışında.
FlySwat

7
Bu cevabın her kısmına katılmıyorum; onlar yok ille yığın canlı ve kopya-semantik olan çok farklı sınıflara.
Marc Gravell

1
Değişmezler, aşırı yapı kullanımı hafızanızı
üzecek

1
@Teomanshipahi Sınıf örneklerinin aşırı kullanımı çöp toplayıcınızı çıldırtacaktır.
IllidanS4 Monica'yı

4
20k + rep'e sahip biri için bu cevap kesinlikle kabul edilemez.
Krythic
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.