Kovaryans ve Kontra Varyans Arasındaki Fark


Yanıtlar:


268

Soru, "kovaryans ve kontraverans arasındaki fark nedir?"

Kovaryans ve kontravaryans, bir kümenin bir üyesini diğeriyle ilişkilendiren bir eşleme işlevinin özellikleridir . Daha spesifik olarak, bir eşleme , o küme üzerindeki bir ilişkiye göre eş değişken veya çelişkili olabilir .

Tüm C # türleri kümesinin aşağıdaki iki alt kümesini düşünün. İlk:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

İkincisi, bu açıkça ilişkili set:

{ IEnumerable<Animal>, 
  IEnumerable<Tiger>, 
  IEnumerable<Fruit>, 
  IEnumerable<Banana> }

İlk setten ikinci sete kadar bir haritalama işlemi var. Yani, birinci setteki her T için , ikinci setteki karşılık gelen tiptir IEnumerable<T>. Ya da kısaca haritalama T → IE<T>. Bunun "ince bir ok" olduğuna dikkat edin.

Şimdiye kadar benimle?

Şimdi bir ilişki düşünelim . İlk setteki tip çiftleri arasında bir atama uyumluluğu ilişkisi vardır . Bir tür Tigerdeğişkenine bir tür değeri atanabilir Animal, bu nedenle bu türlerin "atama uyumlu" olduğu söylenir. Hadi yazma "türünde bir değer Xtüründe bir değişkene atanabilir Ydaha kısa formda": X ⇒ Y. Bunun "şişman bir ok" olduğuna dikkat edin.

İlk alt kümemizde, işte tüm atama uyumluluğu ilişkileri:

Tiger  ⇒ Tiger
Tiger  ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit  ⇒ Fruit

Belirli arabirimlerin birlikte değişken atama uyumluluğunu destekleyen C # 4'te, ikinci setteki tür çiftleri arasında bir atama uyumluluğu ilişkisi vardır:

IE<Tiger>  ⇒ IE<Tiger>
IE<Tiger>  ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit>  ⇒ IE<Fruit>

Eşlemenin T → IE<T> , atama uyumluluğunun varlığını ve yönünü koruduğuna dikkat edin . Yani, eğer X ⇒ Yöyleyse bu da doğrudur IE<X> ⇒ IE<Y>.

Şişman bir okun iki tarafında iki şey varsa, o zaman her iki tarafı da ona karşılık gelen ince bir okun sağ tarafındaki bir şeyle değiştirebiliriz.

Belirli bir ilişkiye göre bu özelliğe sahip olan bir eşlemeye "ortak değişken eşleme" denir. Bu mantıklı olmalıdır: Bir dizi Hayvanın gerekli olduğu yerlerde bir Kaplan dizisi kullanılabilir, ancak bunun tersi doğru değildir. Bir dizi kaplanın gerekli olduğu durumlarda bir dizi hayvan kullanılamaz.

Bu kovaryans. Şimdi tüm türler kümesinin bu alt kümesini düşünün:

{ IComparable<Tiger>, 
  IComparable<Animal>, 
  IComparable<Fruit>, 
  IComparable<Banana> }

şimdi ilk setten üçüncü sete kadar eşleştirmeye sahibiz T → IC<T>.

C # 4'te:

IC<Tiger>  ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger>     Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit>  ⇒ IC<Banana>     Backwards!
IC<Fruit>  ⇒ IC<Fruit>

Kendisine, eşleme T → IC<T>vardır varlığını sürdüren ancak yönünü tersine atama uyumluluğu. Yani, eğer X ⇒ Yöyleyse IC<X> ⇐ IC<Y>.

Bir ilişkiyi koruyan ancak tersine çeviren bir eşlemeye , aykırı eşleme denir .

Yine, bu açıkça doğru olmalıdır. İki hayvanı karşılaştırabilen bir cihaz aynı zamanda iki Kaplanı da karşılaştırabilir, ancak iki Kaplanı karşılaştırabilen bir cihaz, herhangi iki hayvanı karşılaştırmak zorunda değildir.

C # 4'teki kovaryans ve kontravaryans arasındaki fark budur. Kovaryans atanabilirliğin yönünü korur . Contravariance bunu tersine çevirir .


4
Benim gibi biri için neyin eşdeğişken OLMADIĞINI ve neyin çelişkili OLMADIĞINI ve her ikisinin de OLMADIĞINI gösteren örnekler eklemek daha iyi olurdu.
bjan

3
@Bargitta: Çok benzer. Aradaki fark, C # 'nin kesin site varyansı kullanması ve Java'nın çağrı sitesi varyansı kullanmasıdır . Yani işlerin değişme şekli aynıdır, ancak geliştiricinin "Bunun varyant olması için ihtiyacım var" dediği yer farklıdır. Bu arada, her iki dildeki özellik kısmen aynı kişi tarafından tasarlandı!
Eric Lippert

2
@AshishNegi: Oku "olarak kullanılabilir" olarak okuyun. "Hayvanları karşılaştırabilen bir şey, kaplanları karşılaştırabilecek bir şey olarak kullanılabilir". Şimdi mantıklı mı?
Eric Lippert

2
@AshishNegi: Hayır, bu doğru değil. IEnumerable ortak değişkendir çünkü T yalnızca IEnumerable yöntemlerinin dönüşlerinde görünür. Ve IComparable çelişkilidir çünkü T yalnızca IComparable yöntemlerinin biçimsel parametreleri olarak görünür .
Eric Lippert

3
@AshishNegi: Bu ilişkilerin altında yatan mantıksal nedenleri düşünmek istiyorsunuz . Neden biz dönüştürebilirsiniz IEnumerable<Tiger>için IEnumerable<Animal>güvenli? Yolu yoktur olduğundan girişine bir zürafa içine IEnumerable<Animal>. Neden bir dönüştürebilirsiniz IComparable<Animal>için IComparable<Tiger>? Yolu yoktur Çünkü çıkar bir bir zürafa IComparable<Animal>. Mantıklı olmak?
Eric Lippert

112

Örnek vermek muhtemelen en kolayı - kesinlikle onları böyle hatırlıyorum.

Kovaryans

Kanonik örnekler: IEnumerable<out T>,Func<out T>

Sen dönüştürebilirsiniz IEnumerable<string>için IEnumerable<object>veya Func<string>hiç Func<object>. Yalnızca değerler gel dışarı bu nesneler.

İşe yarıyor, çünkü yalnızca API'den değerler alıyorsanız ve belirli bir şey (gibi string) döndürecekse , döndürülen değeri daha genel bir tür (gibi object) olarak ele alabilirsiniz.

Kontraviyans

Kanonik örnekler: IComparer<in T>,Action<in T>

Sen dönüştürebilirsiniz IComparer<object>için IComparer<string>, ya Action<object>hiç Action<string>; değerleri sadece gitmek içine bu nesnelerin.

Bu sefer işe yarıyor çünkü API genel bir şey bekliyorsa (gibi object) ona daha spesifik (gibi string) bir şey verebilirsiniz .

Daha genel olarak

Bir arabirim varsa IFoo<T>içeri bildirdiğinden olabilir T(yani ilan IFoo<out T>eğer Tsadece arayüzünde (örneğin bir döndürme türü) bir çıkış pozisyonunda kullanılır. Bu içinde kontravaryant olabilir T(yani IFoo<in T>varsa) Tsadece bir giriş pozisyonunda kullanılır ( örneğin bir parametre türü).

Potansiyel olarak kafa karıştırıcı hale geliyor çünkü "çıktı konumu" göründüğü kadar basit değil - bir tür parametresi Action<T>hala yalnızca Tbir çıktı konumunda kullanılıyor - Action<T>ne demek istediğimi anlarsanız, onu tersine çevirir. Bu değerler yöntemin uygulanmasından geçirebilmesi bir "çıkış" olduğunu doğru bir dönüş değeri kutusu gibi, arayanın kodu. Neyse ki genellikle bu tür şeyler ortaya çıkmaz :)


1
Benim gibi biri için, neyin ortak değişken OLMADIĞINI ve neyin çelişkili OLMADIĞINI ve her ikisinin de OLMADIĞINI gösteren örnekler eklemek daha iyi olurdu.
bjan

1
@Jon Skeet Güzel örnek, "bir tür parametresi Action<T>hala yalnızca Tbir çıktı konumunda kullanılıyor " anlamıyorum . Action<T>dönüş türü geçersizdir, nasıl Tçıktı olarak kullanılabilir? Yoksa bu, hiçbir şey döndürmediği için mi kuralı ihlal edemeyeceğini görebiliyor musunuz?
Alexander Derck

2
Bu mükemmel cevap arkasını geliyor benim gelecekteki öz, To tekrar : farkını relearn, bu istediğiniz çizgidir sadece API üzerinden değerleri alıyorsun çünkü eğer [Kovaryans] çalıştığını", ve bir şey dönmek için gidiyor belirli (dize gibi), döndürülen değeri daha genel bir tür (nesne gibi) olarak değerlendirebilirsiniz. "
Matt Klein

Tüm bunların en kafa karıştırıcı kısmı, kovaryans veya kontravaryans için, yönü (içeri veya dışarı) görmezden gelirseniz, yine de Daha Genel dönüşüme Daha Özel olursunuz! Demek istediğim: kovaryans için "döndürülen bu değeri daha genel bir tür (nesne gibi) olarak ele alabilirsiniz" ve: "API genel bir şey bekliyor (nesne gibi), ona daha spesifik bir şey verebilirsiniz (dizge gibi)" kontravans için . Benim için bu ses biraz aynı!
XMight

@AlexanderDerck: Neden daha önce cevap vermediğimden emin değilim; Belirsiz olduğuna katılıyorum ve bunu netleştirmeye çalışacağım.
Jon Skeet

16

Umarım yazım konunun dilden bağımsız bir görünümünü elde etmeye yardımcı olur.

İç eğitimlerimiz için harika bir kitap olan "Smalltalk, Objects and Design (Chamond Liu)" ile çalıştım ve aşağıdaki örnekleri yeniden ifade ettim.

"Tutarlılık" ne anlama geliyor? Buradaki fikir, yüksek oranda ikame edilebilir türlerle tür güvenli tür hiyerarşileri tasarlamaktır. Statik olarak yazılmış bir dilde çalışıyorsanız, bu tutarlılığı elde etmenin anahtarı alt tür tabanlı uyumluluktur. (Liskov İkame İlkesini (LSP) burada yüksek düzeyde tartışacağız.)

Pratik örnekler (sözde kod / C # 'da geçersiz):

  • Kovaryans: Statik yazımla Yumurtaları “tutarlı bir şekilde” yumurtlayan Kuşlar varsayalım: Kuş türü bir Yumurta bırakırsa, Kuş'un alt türü Yumurta'nın bir alt türünü koymaz mı? Örneğin Duck tipi bir DuckEgg bırakır, ardından kıvam verilir. Bu neden tutarlı? Çünkü böyle bir ifadede: Egg anEgg = aBird.Lay();aBird referansı yasal olarak bir Bird veya Duck örneği ile değiştirilebilir. Dönüş türünün Lay () 'in tanımlandığı türle eşdeğişken olduğunu söylüyoruz. Bir alt türün geçersiz kılması, daha özel bir tür döndürebilir. => "Daha fazlasını sunarlar."

  • Contravariance: Piyanoların, Piyanistlerin statik daktiloyla "tutarlı" çalabileceğini varsayalım: Bir Piyanist, Piyano çalarsa, Kuyruklu Piyano çalabilir mi? Bir Virtüöz bir kuyruklu piyano çalmayı tercih etmez mi? (Dikkat edin; bir bükülme var!) Bu tutarsız! Çünkü böyle bir ifadede: aPiano.Play(aPianist);bir Piyano yasal olarak bir Piyano veya bir Kuyruklu Piyano örneğiyle değiştirilemez! Bir Kuyruklu Piyano yalnızca Virtüöz tarafından çalınabilir, Piyanistler çok geneldir! GrandPiano'lar daha genel türler tarafından oynanabilir olmalıdır, bu durumda oyun tutarlıdır. Parametre türünün, Play () 'in tanımlandığı türe aykırı olduğunu söylüyoruz. Bir alt türün geçersiz kılması, daha genelleştirilmiş bir türü kabul edebilir. => "Daha azına ihtiyaç duyarlar."

Geri C #:
C # temelde statik olarak yazılmış bir dil olduğu için, bir türün arayüzünün birlikte veya çelişkili olması gereken "konumları" (örn. Parametreler ve dönüş türleri), bu türün tutarlı bir şekilde kullanılmasını / geliştirilmesini garanti etmek için açıkça işaretlenmelidir. , LSP'nin düzgün çalışmasını sağlamak için. Dinamik olarak yazılan dillerde LSP tutarlılığı tipik olarak bir sorun değildir, diğer bir deyişle, türlerinizde yalnızca dinamik türü kullandıysanız, .Net arayüzlerinde ve temsilcilerinde eş ve karşıt "işaretlemeden" tamamen kurtulabilirsiniz. - Ancak bu C # için en iyi çözüm değildir (genel arayüzlerde dinamik kullanmamalısınız).

Teoriye
geri dönün : Tanımlanan uygunluk (kovaryant dönüş türleri / karşıt değişken parametre türleri) teorik idealdir (Emerald ve POOL-1 dilleri tarafından desteklenir). Bazı oop dilleri (örn. Eyfel) başka bir tutarlılık türü uygulamaya karar verdi, özellikle. aynı zamanda kovaryant parametre türleri, çünkü gerçekliği teorik idealden daha iyi tanımlıyor. Statik olarak yazılmış dillerde, istenen tutarlılık genellikle "çift gönderim" ve "ziyaretçi" gibi tasarım modellerinin uygulanmasıyla elde edilmelidir. Diğer diller sözde "çoklu gönderme" veya çoklu yöntemler sağlar (bu temelde çalışma zamanında işlev aşırı yüklerini seçmektir , örneğin CLOS ile) veya dinamik yazmayı kullanarak istenen etkiyi elde eder.


Bir alt türün geçersiz kılma işleminin daha özel bir tür döndürebileceğini söylüyorsunuz . Ancak bu tamamen yanlıştır. Eğer Birdtanımlayıp public abstract BirdEgg Lay();, daha sonra Duck : Bird GEREKİR uygulamak public override BirdEgg Lay(){}için iddia Yani BirdEgg anEgg = aBird.Lay();hiç Varyans her türlü vardır ki gerçeği yansıtmamaktadır. Açıklamanın dayanak noktası olarak, tüm nokta artık ortadan kalktı. Bunun yerine , bir DuckEgg'in BirdEgg out / return türüne örtük olarak atıldığı uygulamada kovaryansın var olduğunu söyleyebilir misiniz ? Her iki durumda da lütfen kafa karışıklığımı giderin.
Suamere

1
Kısaca söylemek gerekirse: haklısın! Karışıklık için özür dilerim. C #DuckEgg Lay() için geçerli bir geçersiz kılma değildir ve işin püf noktası budur. C # ortak değişken dönüş türlerini desteklemez, ancak Java ve C ++ destekler. Daha ziyade teorik ideali C # benzeri bir sözdizimi kullanarak tanımladım. C # 'da Bird ve Duck'ın ortak bir arabirim uygulamasına izin vermeniz gerekir; burada Lay, bir ortak değişken dönüş (yani dış özellik) türüne sahip olacak şekilde tanımlanır, sonra konular birbirine uyar! Egg Lay()
Nico

1
Matt-Klein'ın @ Jon-Skeet'in cevabına ilişkin yorumuna bir analog olarak, "gelecekteki benliğime": Burada benim için en iyi paket, "Daha fazlasını veriyorlar" (spesifik) ve "Daha azına ihtiyaç duyuyorlar" (spesifik). "Daha az isteyin ve daha fazlasını sunun" mükemmel bir anımsatıcıdır! Daha az spesifik talimatlar (genel istekler) gerektirmeyi ve yine de daha spesifik bir şey (gerçek bir çalışma ürünü) sunmayı umduğum bir işe benzer. Her iki durumda da alt türlerin sırası (LSP) kesintisizdir.
karfus

@karfus: Teşekkür ederim, ama hatırladığım kadarıyla başka bir kaynaktan "Daha az talep et ve daha fazlasını teslim et" fikrini başka kelimelerle ifade ettim. Yukarıda bahsettiğim Liu'nun kitabı olabilir ... hatta bir .NET Rock konuşması olabilir. Btw. Java'da insanlar anımsatıcıyı, varyansları bildirmenin sözdizimsel yolu ile doğrudan ilişkili olan "PECS" e indirgedi, PECS "Üretici extends, Tüketici super" içindir.
Nico

5

Dönüştürücü temsilcisi farkı anlamama yardımcı oluyor.

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputbir yöntemin daha spesifik bir tür döndürdüğü kovaryansı temsil eder .

TInputBir yöntemin daha az spesifik bir türden geçtiği kontravansı temsil eder .

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();

1

Co ve Contra varyansı oldukça mantıklı şeylerdir. Dil tipi sistemi bizi gerçek hayat mantığını desteklemeye zorlar. Örnek olarak anlamak kolaydır.

Kovaryans

Örneğin bir çiçek almak istiyorsunuz ve şehrinizde iki çiçekçi dükkanınız var: gül dükkanı ve papatya dükkanı.

Birine "çiçekçi nerede?" Diye sorarsan ve birisi sana gül dükkanının nerede olduğunu söyler, sorun olur mu? Evet, gül çiçek olduğu için çiçek almak isterseniz gül satın alabilirsiniz. Aynı durum, birisi size papatya dükkanının adresini vermişse de geçerlidir.

Bu bir örnektir kovaryans : Eğer döküm için izin verilen A<C>için A<B>nerede, Cbir alt sınıfı olan Bise, A(işlevinden sonucunda döner) jenerik değerler üretir. Kovaryans üreticilerle ilgilidir, bu yüzden C # outkovaryans için anahtar kelimeyi kullanır .

Türler:

class Flower {  }
class Rose: Flower { }
class Daisy: Flower { }

interface FlowerShop<out T> where T: Flower {
    T getFlower();
}

class RoseShop: FlowerShop<Rose> {
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop: FlowerShop<Daisy> {
    public Daisy getFlower() {
        return new Daisy();
    }
}

Soru "çiçekçi nerede?", Cevap "orada gül dükkanı":

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

Kontraviyans

Mesela kız arkadaşına bir çiçek hediye etmek istiyorsun ve kız arkadaşın her çiçeği seviyor. Onu gülleri seven biri olarak mı yoksa papatyaları seven biri olarak düşünebilir misin? Evet, çünkü herhangi bir çiçeği seviyorsa hem gülü hem de papatyayı severdi.

Bu bir örnektir contravariance : Eğer döküm için izin konum A<B>için A<C>, Calt sınıfını ise Beğer, Atükettiğini jenerik değer. Contravariance tüketicilerle ilgilidir, bu yüzden C # inkontravariance anahtar kelimesini kullanır .

Türler:

interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
    void takeGift(TFavoriteFlower flower);
}

class AnyFlowerLover: PrettyGirl<Flower> {
    public void takeGift(Flower flower) {
        Console.WriteLine("I like all flowers!");
    }
}

Herhangi bir çiçeği seven kız arkadaşınızı gülleri seven ve ona bir gül veren biri olarak düşünüyorsunuz:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

Bağlantılar

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.