ฉันมีปัญหาในการทำความเข้าใจความแตกต่างระหว่างความแปรปรวนร่วมและความแตกต่าง
ฉันมีปัญหาในการทำความเข้าใจความแตกต่างระหว่างความแปรปรวนร่วมและความแตกต่าง
คำตอบ:
คำถามคือ "ความแปรปรวนร่วมและความแตกต่างคืออะไร"
ความแปรปรวนและ contravariance เป็นทรัพย์สินของฟังก์ชั่นการทำแผนที่ที่ร่วมงานหนึ่งในสมาชิกของชุดอีกด้วย โดยเฉพาะอย่างยิ่งการทำแผนที่สามารถแปรปรวนร่วมหรือเทียบเคียงกับความสัมพันธ์ในชุดนั้น
พิจารณาเซตย่อยสองชุดต่อไปนี้ของชุดทุกประเภท C # ครั้งแรก:
{ Animal,
Tiger,
Fruit,
Banana }.
และประการที่สองชุดนี้เกี่ยวข้องอย่างชัดเจน:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
มีการดำเนินการแมปจากชุดแรกเป็นชุดที่สอง นั่นคือสำหรับแต่ละ T ในชุดแรกที่สอดคล้องกันIEnumerable<T>ชนิดในชุดที่สองคือ T → IE<T>หรือในรูปแบบระยะสั้นการทำแผนที่เป็น ขอให้สังเกตว่านี่คือ "ลูกศรบาง"
กับฉันจนถึงตอนนี้
ลองพิจารณาความสัมพันธ์กัน มีความสัมพันธ์ความเข้ากันได้ที่ได้รับมอบหมายระหว่างคู่ประเภทในชุดแรกคือ ค่าประเภทTigerสามารถกำหนดให้กับตัวแปรประเภทAnimalดังนั้นประเภทเหล่านี้จะกล่าวว่า "เข้ากันได้กับการกำหนด" ลองเขียน "ค่าประเภทXสามารถกำหนดให้กับตัวแปรประเภทY" ในรูปแบบที่สั้นกว่า:X ⇒ Yในรูปแบบสั้น: ขอให้สังเกตว่านี่คือ "ลูกศรอ้วน"
ดังนั้นในชุดย่อยแรกของเรานี่คือความสัมพันธ์ที่เข้ากันได้ของการกำหนด:
Tiger ⇒ Tiger
Tiger ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit ⇒ Fruit
ใน C # 4 ซึ่งรองรับความเข้ากันได้ของการกำหนด covariant ของอินเทอร์เฟซบางอย่างมีความสัมพันธ์ความเข้ากันได้ที่ได้รับมอบหมายระหว่างคู่ของประเภทในชุดที่สอง:
IE<Tiger> ⇒ IE<Tiger>
IE<Tiger> ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit> ⇒ IE<Fruit>
ขอให้สังเกตว่าการทำแผนที่การดำรงไว้ซึ่งการดำรงอยู่และทิศทางของการทำงานร่วมกันที่ได้รับมอบหมายT → IE<T> นั่นคือถ้าหากX ⇒ Yมันเป็นจริงเช่นกันIE<X> ⇒ IE<Y>เช่นกัน
หากเรามีสองสิ่งที่ด้านใดด้านหนึ่งของลูกศรอ้วนเราสามารถแทนที่ทั้งสองด้านด้วยสิ่งที่อยู่ทางด้านขวามือของลูกศรบางที่สอดคล้องกัน
การแมปที่มีคุณสมบัตินี้เทียบกับความสัมพันธ์เฉพาะเรียกว่า "การทำแผนที่ covariant" สิ่งนี้ควรสมเหตุสมผล: ลำดับของเสือสามารถใช้เมื่อต้องการลำดับของสัตว์ แต่สิ่งที่ตรงกันข้ามนั้นไม่เป็นความจริง ไม่สามารถใช้ลำดับของสัตว์ในกรณีที่จำเป็นต้องใช้ลำดับของเสือ
นั่นคือความแปรปรวนร่วม ตอนนี้ให้พิจารณาเซตย่อยของชุดทุกประเภท:
{ IComparable<Tiger>,
IComparable<Animal>,
IComparable<Fruit>,
IComparable<Banana> }
T → IC<T>ตอนนี้เรามีการทำแผนที่จากชุดแรกกับชุดที่สาม
ใน C # 4:
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>
นั่นคือการแมปT → IC<T>ได้รักษาการดำรงอยู่ แต่กลับทิศทางของความเข้ากันได้ที่ได้รับมอบหมาย นั่นคือถ้าX ⇒ Yแล้วIC<X> ⇐ IC<Y>แล้ว
การทำแผนที่ที่เก็บรักษา แต่กลับความสัมพันธ์ที่เรียกว่าการทำแผนที่contravariant
อีกครั้งนี้ควรถูกต้องชัดเจน อุปกรณ์ที่สามารถเปรียบเทียบสัตว์สองตัวยังสามารถเปรียบเทียบเสือสองตัวได้ แต่อุปกรณ์ที่สามารถเปรียบเทียบเสือสองตัวนั้นไม่สามารถเปรียบเทียบสัตว์สองตัวใด ๆ ได้
นั่นคือความแตกต่างระหว่างความแปรปรวนร่วมและความแปรปรวนใน C # 4 ความแปรปรวนร่วมรักษาทิศทางของการมอบหมาย ความแปรปรวนตรงกันข้ามมัน
IEnumerable<Tiger>ถึงเปลี่ยนเป็นIEnumerable<Animal>ปลอดภัย? เพราะไม่มีทางที่จะป้อนข้อมูลIEnumerable<Animal>ยีราฟลง ทำไมเราสามารถแปลงIComparable<Animal>ไปIComparable<Tiger>? เพราะไม่มีทางที่จะไม่นำออกIComparable<Animal>ยีราฟจาก ทำให้รู้สึก?
อาจเป็นตัวอย่างที่ง่ายที่สุด - นั่นเป็นวิธีที่ฉันจำได้ง่าย
แปรปรวน
ตัวอย่างที่ยอมรับได้: IEnumerable<out T>,Func<out T>
คุณสามารถแปลงจากIEnumerable<string>ไปIEnumerable<object>หรือเพื่อFunc<string> Func<object>ค่าจะออกมาจากวัตถุเหล่านี้เท่านั้น
ใช้งานได้เพราะหากคุณเพียงนำค่าออกจาก API และจะส่งคืนบางสิ่งที่เฉพาะเจาะจง (เช่นstring) คุณสามารถจัดการกับค่าที่ส่งคืนเป็นประเภททั่วไป (เช่นobject)
contravariance
ตัวอย่างที่ยอมรับได้: IComparer<in T>,Action<in T>
คุณสามารถแปลงจากIComparer<object>เป็นIComparer<string>หรือAction<object>เป็นAction<string>; ค่าเข้าไปในวัตถุเหล่านี้เท่านั้น
เวลานี้ใช้งานได้เพราะหาก API คาดหวังบางสิ่งทั่วไป (เช่นobject) คุณสามารถให้บางสิ่งที่เฉพาะเจาะจงมากขึ้น (เช่นstring)
ให้เป็นปกติมากกว่านี้
หากคุณมีอินเตอร์เฟซIFoo<T>มันสามารถ covariant ในT(เช่นประกาศว่าราวกับIFoo<out T>ว่าTใช้เฉพาะในตำแหน่งเอาต์พุต (เช่นชนิดส่งคืน) ภายในอินเตอร์เฟสมันสามารถ contravariant ในT(เช่นIFoo<in T>) ถ้าTใช้เฉพาะในตำแหน่งอินพุต ( เช่นประเภทพารามิเตอร์)
มันอาจจะสับสนเพราะ "ตำแหน่งเอาต์พุต" นั้นไม่ง่ายอย่างที่คิด - พารามิเตอร์ประเภทAction<T>ยังคงใช้เฉพาะTในตำแหน่งเอาต์พุต - ความแปรปรวนของการAction<T>หมุนรอบหากคุณเห็นสิ่งที่ฉันหมายถึง มันเป็น "เอาท์พุท" ในการที่ค่าสามารถส่งผ่านจากการใช้วิธีการที่มีต่อรหัสของผู้โทรเช่นเดียวกับค่าตอบแทนสามารถ โดยปกติแล้วสิ่งแบบนี้จะไม่เกิดขึ้นโชคดี :)
Action<T>ยังคงใช้เฉพาะTในตำแหน่งเอาต์พุต"เท่านั้น Action<T>return type เป็นโมฆะมันจะใช้Tเป็น output ได้อย่างไร? หรือว่ามันหมายความว่าอะไรเพราะมันไม่ส่งคืนสิ่งใดที่คุณเห็นว่ามันไม่สามารถละเมิดกฎได้?
ฉันหวังว่าโพสต์ของฉันจะช่วยให้ได้มุมมองที่ไม่เชื่อเรื่องภาษา
สำหรับการฝึกอบรมภายในของฉันฉันได้ทำงานกับหนังสือ "Smalltalk, Objects and Design (Chamond Liu)" ที่ยอดเยี่ยมและฉันขอแนะนำตัวอย่างต่อไปนี้
“ ความสอดคล้อง” หมายถึงอะไร แนวคิดคือการออกแบบลำดับชั้นชนิดที่ปลอดภัยด้วยชนิดที่ทดแทนได้สูง กุญแจสำคัญในการรับความสอดคล้องนี้คือความสอดคล้องตามประเภทย่อยหากคุณทำงานในภาษาที่พิมพ์แบบคงที่ (เราจะพูดถึงหลักการทดแทน Liskov (LSP) ในระดับสูงที่นี่)
ตัวอย่างการปฏิบัติ (รหัสเทียม / ไม่ถูกต้องใน C #):
ความแปรปรวนร่วม: สมมติว่านกที่วางไข่“ สม่ำเสมอ” ด้วยการพิมพ์แบบคงที่: หากประเภทนกวางไข่ชนิดย่อยของนกจะไม่วางไข่ย่อยหรือไม่? เช่นเป็ดประเภทวาง DuckEgg จากนั้นจะได้รับความสอดคล้อง ทำไมสิ่งนี้จึงสอดคล้องกัน เพราะในนิพจน์ดังกล่าว: Egg anEgg = aBird.Lay();การอ้างอิง aBird อาจถูกแทนที่โดยกฎหมายโดย Bird หรือโดย Duck เราพูดว่า return type เป็น covariant กับประเภทที่ Lay () กำหนดไว้ การแทนที่ของชนิดย่อยอาจส่งคืนชนิดที่พิเศษกว่า =>“ พวกเขาส่งมอบมากขึ้น”
การแปรปรวน: สมมติว่าเปียโนเป็นนักเปียโนที่สามารถเล่น“ สม่ำเสมอ” ด้วยการพิมพ์แบบคงที่: ถ้านักเปียโนเล่นเปียโนเธอจะสามารถเล่น GrandPiano ได้ไหม? จะไม่ Virtuoso เล่น GrandPiano หรือ (ถูกเตือน; มีการบิด!) นี้ไม่สอดคล้องกัน! เพราะในการแสดงออกดังกล่าว: aPiano.Play(aPianist);aPiano ไม่สามารถทดแทนเปียโนอย่างถูกกฎหมายหรือโดยแกรนด์เปียโน GrandPiano! GrandPiano สามารถเล่นได้โดย Virtuoso เท่านั้นนักเปียโนก็กว้างเกินไป! GrandPianos จะต้องเล่นได้ตามประเภททั่วไปมากขึ้นจากนั้นการเล่นจะสอดคล้องกัน เราบอกว่าประเภทพารามิเตอร์นั้นขัดแย้งกับประเภทที่ Play () กำหนดไว้ การแทนที่ของชนิดย่อยอาจยอมรับประเภททั่วไปมากขึ้น =>“ พวกมันต้องการน้อยกว่า”
กลับไปที่ C #:
เนื่องจาก C # เป็นภาษาที่พิมพ์แบบคงที่ "ตำแหน่ง" ของอินเทอร์เฟซของประเภทที่ควรจะร่วมกันหรือ contravariant (เช่นพารามิเตอร์และประเภทที่ส่งคืน) ต้องทำเครื่องหมายอย่างชัดเจนเพื่อรับประกันการใช้งาน / การพัฒนาที่สอดคล้องกัน เพื่อให้ LSP ทำงานได้ดี ในภาษาที่พิมพ์แบบไดนามิกความสอดคล้องของ LSP มักจะไม่เป็นปัญหาในคำอื่น ๆ ที่คุณสามารถกำจัด "มาร์กอัป" แบบร่วมและแบบ contravariant บนอินเทอร์เฟซ. Net และผู้ได้รับมอบหมายหากคุณใช้ประเภทไดนามิกในประเภทของคุณ - แต่นี่ไม่ใช่ทางออกที่ดีที่สุดใน C # (คุณไม่ควรใช้ไดนามิกในส่วนต่อประสานสาธารณะ)
กลับไปที่ทฤษฎี:
ความสอดคล้องที่อธิบายไว้ (ชนิดพารามิเตอร์ส่งคืน covariant / ประเภทพารามิเตอร์ contravariant) เป็นอุดมคติทางทฤษฎี (สนับสนุนโดยภาษา Emerald และ POOL-1) ภาษา oop บางภาษา (เช่น Eiffel) ตัดสินใจใช้ความสอดคล้องประเภทอื่นโดยเฉพาะ ยังพารามิเตอร์ประเภท covariant เพราะมันอธิบายความเป็นจริงได้ดีกว่าอุดมคติทางทฤษฎี ในภาษาที่พิมพ์แบบคงที่ความสอดคล้องที่ต้องการมักจะเกิดขึ้นได้ด้วยการใช้รูปแบบการออกแบบเช่น "การเยี่ยงอย่างสองครั้ง" และ "ผู้เยี่ยมชม" ภาษาอื่น ๆ ให้เรียกว่า "การแจกจ่ายหลายอย่าง" หรือหลายวิธี (โดยทั่วไปการเลือกฟังก์ชั่นการโอเวอร์โหลด ณเวลาทำงานเช่น CLOS) หรือรับผลตามที่ต้องการโดยใช้การพิมพ์แบบไดนามิก
Birdกำหนดไว้public abstract BirdEgg Lay();คุณจะDuck : Bird ต้องดำเนินการpublic override BirdEgg Lay(){}ดังนั้นการยืนยันของคุณที่BirdEgg anEgg = aBird.Lay();มีความแปรปรวนใด ๆ ก็ไม่เป็นความจริง การเป็นจุดเริ่มต้นของคำอธิบายตอนนี้จุดทั้งหมดหายไปแล้ว คุณจะแทนที่จะบอกว่าความแปรปรวนที่มีอยู่ภายในการดำเนินการที่ DuckEgg ถูกโยนไปโดยปริยาย BirdEgg ออกประเภทกลับ / หรือไม่ โปรดกำจัดความสับสนของฉัน
DuckEgg Lay()ไม่ใช่การแทนที่ที่ถูกต้องสำหรับEgg Lay() ใน C #และนั่นคือ crux C # ไม่สนับสนุนประเภทการคืนค่า covariant แต่ Java รวมถึง C ++ ทำได้ ฉันค่อนข้างอธิบายอุดมคติทางทฤษฎีโดยใช้ไวยากรณ์คล้าย C # ใน C # คุณต้องปล่อยให้ Bird และ Duck ใช้อินเทอร์เฟซทั่วไปซึ่ง Lay กำหนดไว้เพื่อให้ได้ผลตอบแทน covariant (เช่น out-spec) จากนั้นเข้าด้วยกัน!
extends, Consumer super"
ผู้รับมอบสิทธิ์แปลงช่วยให้ฉันเข้าใจความแตกต่าง
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutputแสดงให้เห็นถึงความแปรปรวนที่วิธีการส่งกลับประเภทที่เฉพาะเจาะจงมากขึ้น
TInputแสดงให้เห็นถึงcontravarianceที่วิธีการที่จะผ่านประเภทที่เฉพาะเจาะจงน้อย
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();
ความแปรปรวนของ Co และ Contra นั้นค่อนข้างสมเหตุสมผล ระบบประเภทภาษาบังคับให้เราสนับสนุนตรรกะในชีวิตจริง เป็นตัวอย่างที่เข้าใจง่าย
ตัวอย่างเช่นคุณต้องการซื้อดอกไม้และคุณมีร้านดอกไม้สองแห่งในเมืองของคุณ: ร้านกุหลาบและร้านดอกเดซี่
ถ้าคุณถามใครสักคน "ร้านขายดอกไม้อยู่ที่ไหน" และมีคนบอกคุณว่าร้านกุหลาบอยู่ที่ไหน ใช่เพราะดอกกุหลาบเป็นดอกไม้ถ้าคุณต้องการซื้อดอกไม้คุณสามารถซื้อดอกกุหลาบได้ เช่นเดียวกันถ้ามีคนตอบคุณด้วยที่อยู่ของร้านเดซี่
นี่คือตัวอย่างของความแปรปรวนร่วม : คุณได้รับอนุญาตให้ส่งA<C>ไปA<B>ที่ซึ่งCเป็นคลาสย่อยของBหากAสร้างค่าทั่วไป (ส่งคืนเป็นผลมาจากฟังก์ชั่น) ความแปรปรวนร่วมเป็นเรื่องเกี่ยวกับผู้ผลิตนั่นคือเหตุผลที่ C # ใช้คำสำคัญoutเพื่อความแปรปรวนร่วม
ประเภท:
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();
}
}
คำถามคือ "ร้านขายดอกไม้อยู่ที่ไหน" คำตอบคือ "ร้านขายของที่นั่น":
static FlowerShop<Flower> tellMeShopAddress() {
return new RoseShop();
}
ตัวอย่างเช่นคุณต้องการของขวัญดอกไม้ให้แฟนของคุณและ Girlfrend ของคุณชอบดอกไม้ใด ๆ คุณคิดว่าเธอเป็นคนที่รักดอกกุหลาบหรือเป็นคนที่รักดอกเดซี่หรือไม่? ใช่เพราะถ้าเธอรักดอกไม้ใด ๆ เธอจะรักทั้งดอกกุหลาบและดอกเดซี่
นี่คือตัวอย่างของที่contravariance : คุณได้รับอนุญาตให้โยนA<B>ไปA<C>ที่Cเป็น subclass ของBถ้าAกินค่าทั่วไป ความแปรปรวนเป็นเรื่องเกี่ยวกับผู้บริโภคนั่นคือเหตุผลที่ C # ใช้คำสำคัญinเพื่อความแตกต่าง
ประเภท:
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!");
}
}
คุณกำลังพิจารณาแฟนสาวของคุณที่รักดอกไม้ใด ๆ ในฐานะคนที่รักดอกกุหลาบและให้ดอกกุหลาบแก่เธอ:
PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());