ฉันมีปัญหาในการทำความเข้าใจความแตกต่างระหว่างความแปรปรวนร่วมและความแตกต่าง
ฉันมีปัญหาในการทำความเข้าใจความแตกต่างระหว่างความแปรปรวนร่วมและความแตกต่าง
คำตอบ:
คำถามคือ "ความแปรปรวนร่วมและความแตกต่างคืออะไร"
ความแปรปรวนและ 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());