Generics vs อินเตอร์เฟสทั่วไป?


20

ฉันจำไม่ได้เมื่อฉันเขียนชั้นเรียนทั่วไปครั้งที่แล้ว ทุกครั้งที่ฉันคิดว่าฉันต้องการมันหลังจากที่ฉันคิดว่าจะทำข้อสรุปที่ฉันทำไม่ได้

คำตอบที่สองสำหรับคำถามนี้ทำให้ฉันต้องขอคำชี้แจง (เนื่องจากฉันยังไม่สามารถแสดงความคิดเห็นได้ฉันจึงได้ตั้งคำถามใหม่)

ดังนั้นลองใช้โค้ดที่ได้รับเป็นตัวอย่างของกรณีที่ใครต้องการยาชื่อสามัญ:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

มันมีข้อ จำกัด ประเภท: IBusinessObject

วิธีคิดตามปกติของฉันคือคลาสมีข้อ จำกัด ในการใช้IBusinessObjectดังนั้นคลาสที่ใช้สิ่งนี้Repositoryคือ พื้นที่เก็บข้อมูลเก็บลูกค้าเหล่านี้IBusinessObjectส่วนใหญ่ของสิ่งนี้Repositoryจะต้องการรับและใช้วัตถุผ่านIBusinessObjectอินเตอร์เฟซ ดังนั้นทำไมไม่เพียงเพื่อ

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

ถึงแม้ว่าตัวอย่างจะไม่ดีเนื่องจากเป็นเพียงคอลเล็กชันอื่นและคอลเล็กชันทั่วไปคือคลาสสิก ในกรณีนี้ประเภทของข้อ จำกัด ดูแปลกเกินไป

ในความเป็นจริงตัวอย่างclass Repository<T> where T : class, IBusinessbBjectมีลักษณะคล้ายclass BusinessObjectRepositoryกับฉันมาก ซึ่งเป็นสิ่งที่ generics ทำเพื่อแก้ไข

ประเด็นทั้งหมดคือ: generics ดีสำหรับทุกอย่างยกเว้นคอลเลกชันและไม่ จำกัด ประเภทที่ทำให้เป็นเรื่องทั่วไปโดยเฉพาะเนื่องจากการใช้ข้อ จำกัด ประเภทนี้แทนที่จะเป็นพารามิเตอร์ประเภททั่วไปในชั้นเรียน

คำตอบ:


24

ก่อนอื่นเรามาพูดเกี่ยวกับพาราเมทริกพหุสัณฐานที่บริสุทธิ์

ตัวแปรหลายตัวแปร

ตัวแปรหลายตัวแปรหมายความว่าอย่างไร ก็หมายความว่าประเภทหรือตัวสร้างประเภทค่อนข้างแปรผันตามประเภท เนื่องจากชนิดถูกส่งเป็นพารามิเตอร์คุณไม่สามารถทราบล่วงหน้าได้ว่ามันอาจเป็นอะไร คุณไม่สามารถตั้งสมมติฐานใด ๆ ทีนี้ถ้าคุณไม่รู้ว่ามันคืออะไรแล้วการใช้งานคืออะไร? คุณทำอะไรกับมันได้บ้าง

ตัวอย่างเช่นคุณสามารถจัดเก็บและเรียกคืนได้ นั่นเป็นกรณีที่คุณพูดถึงแล้ว: คอลเล็กชัน ในการจัดเก็บรายการในรายการหรืออาร์เรย์ฉันไม่จำเป็นต้องรู้อะไรเกี่ยวกับสินค้า รายการหรืออาร์เรย์สามารถลบล้างชนิด

แต่สิ่งที่เกี่ยวกับMaybeประเภท? หากคุณไม่คุ้นเคยกับมันMaybeเป็นประเภทที่อาจมีค่าและอาจไม่ คุณจะใช้มันที่ไหน ตัวอย่างเช่นเมื่อนำไอเท็มออกจากพจนานุกรมความจริงที่ว่าไอเท็มอาจไม่ได้อยู่ในพจนานุกรมนั้นไม่ใช่สถานการณ์ที่พิเศษดังนั้นคุณไม่ควรทิ้งข้อยกเว้นหากไม่มีไอเท็มนั้น แต่คุณกลับตัวอย่างของชนิดย่อยของหนึ่งMaybe<T>ซึ่งมีตรงสองชนิดย่อย: และNone เป็นผู้สมัครของสิ่งที่จริง ๆ ควรกลับแทนการทิ้งข้อยกเว้นหรือการเต้นรำทั้งหมดSome<T>int.ParseMaybe<int>int.TryParse(out bla)

ตอนนี้คุณอาจโต้แย้งว่าMaybekinda-sorta เหมือนรายการที่สามารถมีศูนย์หรือองค์ประกอบเดียวเท่านั้น และมันก็เป็นคอลเล็กชั่น

ถ้าเช่นนั้นจะเป็นTask<T>อย่างไร มันเป็นประเภทที่สัญญาว่าจะคืนค่าในบางจุดในอนาคต แต่ไม่จำเป็นต้องมีค่าในตอนนี้

หรือเกี่ยวกับFunc<T, …>อะไร คุณจะแสดงแนวคิดของฟังก์ชันจากประเภทหนึ่งไปยังอีกประเภทหนึ่งได้อย่างไรถ้าคุณไม่สามารถสรุปนามธรรมได้

หรือมากกว่าโดยทั่วไป: การพิจารณาว่านามธรรมและการนำกลับมาใช้ใหม่เป็นงานพื้นฐานสองประการของวิศวกรรมซอฟต์แวร์ทำไมคุณไม่ต้องการให้นามธรรมเกินประเภท?

ความแตกต่างหลากหลาย

ตอนนี้เรามาพูดถึงพหุนามที่มีขอบเขต polymorphism bounded เป็นพื้นที่แตกต่างคณิตศาสตร์และชนิดย่อย polymorphism พบ: แทนที่จะเป็นตัวสร้างประเภทเป็นสมบูรณ์ลืมเกี่ยวกับพารามิเตอร์ชนิดของคุณสามารถผูก (หรืออุปสรรค) ประเภทที่จะเป็นชนิดย่อยของประเภทที่ระบุไว้บางส่วน

กลับไปที่คอลเล็กชัน ใช้ hashtable เรากล่าวไว้ข้างต้นว่ารายการไม่จำเป็นต้องรู้อะไรเกี่ยวกับองค์ประกอบ ดี hashtable ไม่: มันจำเป็นต้องรู้ว่ามันสามารถแฮชพวกเขา (หมายเหตุ: ใน C # วัตถุทั้งหมดสามารถแฮชได้เหมือนกับวัตถุทั้งหมดที่สามารถนำมาเปรียบเทียบเพื่อความเท่าเทียมกันได้ซึ่งไม่เป็นความจริงสำหรับทุกภาษาแม้ว่าบางครั้งจะถือว่าผิดพลาดในการออกแบบแม้ใน C #)

ดังนั้นคุณต้องการ จำกัด พารามิเตอร์ประเภทของคุณสำหรับประเภทคีย์ใน hashtable ให้เป็นตัวอย่างของIHashable:

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

ลองนึกภาพถ้าคุณมีสิ่งนี้:

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

คุณจะทำอะไรกับvalueคุณออกไปจากที่นั่น? คุณไม่สามารถทำอะไรกับมันได้คุณรู้แค่ว่ามันเป็นวัตถุ และถ้าคุณทำซ้ำมันสิ่งที่คุณได้คือคู่ของสิ่งที่คุณรู้คือIHashable(ซึ่งไม่ได้ช่วยอะไรมากเพราะมันมีคุณสมบัติเดียวเท่านั้นHash) และสิ่งที่คุณรู้คือobject(ซึ่งช่วยคุณได้น้อยลง)

หรือบางสิ่งตามตัวอย่างของคุณ:

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

รายการจะต้องต่อเนื่องได้เนื่องจากกำลังจะถูกเก็บไว้ในดิสก์ แต่ถ้าคุณมีสิ่งนี้แทน:

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

กับกรณีทั่วไปถ้าคุณใส่BankAccountในคุณจะได้รับBankAccountกลับมาพร้อมกับวิธีการและคุณสมบัติชอบOwner, AccountNumber, Balance, Deposit, Withdrawฯลฯ สิ่งที่คุณสามารถทำงานร่วมกับ ตอนนี้กรณีอื่น ๆ ? คุณใส่ในBankAccountแต่คุณได้รับกลับซึ่งมีเพียงหนึ่งในสถานที่ให้บริการ:Serializable AsStringคุณจะทำอะไรกับสิ่งนั้น

นอกจากนี้ยังมีเทคนิคบางอย่างที่คุณสามารถทำได้ด้วย polymorphism ที่มีขอบเขต:

ความแตกต่างหลากหลาย F-bounded

ปริมาณ F-bounded นั้นโดยทั่วไปแล้วตัวแปรประเภทจะปรากฏขึ้นอีกครั้งในข้อ จำกัด สิ่งนี้มีประโยชน์ในบางสถานการณ์ เช่นคุณจะเขียนICloneableอินเตอร์เฟสได้อย่างไร? คุณจะเขียนวิธีที่ชนิดคืนค่าเป็นประเภทของคลาสที่ใช้งานได้อย่างไร ในภาษาที่มีคุณสมบัติMyTypeนั้นเป็นเรื่องง่าย:

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

ในภาษาที่มีความแตกต่างหลากหลายคุณสามารถทำสิ่งนี้แทน:

interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

โปรดทราบว่านี่ไม่ปลอดภัยเท่ารุ่น MyType เพราะไม่มีอะไรหยุดใครบางคนจากการส่งผ่านคลาส "ผิด" ไปยังตัวสร้างประเภท:

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

ประเภทสมาชิกบทคัดย่อ

ตามที่ปรากฎถ้าคุณมีสมาชิกประเภทนามธรรมและ subtyping คุณสามารถได้รับโดยสมบูรณ์โดยไม่ต้องมีตัวแปรหลากหลายและยังคงทำสิ่งเดียวกันทั้งหมด Scala กำลังมุ่งหน้าไปในทิศทางนี้เป็นภาษาหลักแรกที่เริ่มต้นด้วยชื่อสามัญและจากนั้นพยายามที่จะลบออกซึ่งเป็นวิธีอื่น ๆ เช่น Java และ C #

โดยทั่วไปใน Scala เช่นเดียวกับคุณสามารถมีเขตข้อมูลและคุณสมบัติและวิธีการเป็นสมาชิกคุณสามารถมีประเภท และเช่นเดียวกับฟิลด์และคุณสมบัติและวิธีการที่สามารถปล่อยให้เป็นนามธรรมเพื่อนำไปใช้ในคลาสย่อยในภายหลังสมาชิกประเภทยังสามารถเป็นนามธรรมได้ ลองกลับไปที่คอลเล็กชันง่ายๆListว่าจะมีหน้าตาแบบนี้ถ้ามันรองรับใน C #:

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics

ฉันเข้าใจว่าสิ่งที่เป็นนามธรรมมากกว่าประเภทนั้นมีประโยชน์ ฉันแค่ไม่เห็นการใช้งานใน "ชีวิตจริง" Func <> และ Task <> และ Action <> เป็นประเภทไลบรารี และขอบคุณฉันจำได้เกี่ยวกับinterface IFoo<T> where T : IFoo<T>ยัง เห็นได้ชัดว่าเป็นแอปพลิเคชันในชีวิตจริง ตัวอย่างที่ดี แต่ด้วยเหตุผลบางอย่างฉันไม่พอใจ ฉันต้องการที่จะได้รับใจของฉันไปรอบ ๆ เมื่อมันเหมาะสมและเมื่อมันไม่ คำตอบที่นี่มีส่วนร่วมในกระบวนการนี้ แต่ฉันยังรู้สึกว่าตัวเองไม่ได้รอบนี้ มันแปลกเพราะปัญหาระดับภาษาไม่ได้รบกวนฉันมานานแล้ว
jungle_mole

ตัวอย่างที่ดี ฉันรู้สึกเหมือนฉันกลับมาในห้องเรียน +1
Chef_Code

1
@Chef_Code: ฉันหวังว่าจะเป็นคำชม :-P
Jörg W Mittag

ใช่แล้ว. ฉันคิดในภายหลังว่าจะรับรู้ได้อย่างไรหลังจากที่ฉันได้แสดงความคิดเห็นแล้ว ดังนั้นเพื่อยืนยันความจริงใจ ... ใช่มันเป็นคำชมที่ไม่มีอะไรอื่น
Chef_Code

14

ประเด็นทั้งหมดคือ: generics ดีสำหรับทุกอย่างยกเว้นคอลเลกชันและไม่ จำกัด ประเภทที่ทำให้เป็นเรื่องทั่วไปโดยเฉพาะเนื่องจากการใช้ข้อ จำกัด ประเภทนี้แทนที่จะเป็นพารามิเตอร์ประเภททั่วไปในชั้นเรียน

ฉบับที่คุณคิดมากเกินไปเกี่ยวกับการRepositoryที่มันเป็นสวยมากเหมือนกัน แต่นั่นไม่ใช่สิ่งที่มีอยู่ทั่วไป พวกเขากำลังมีสำหรับผู้ใช้

จุดสำคัญที่นี่ไม่ใช่ที่เก็บของนั้นเป็นเรื่องทั่วไปมากกว่า มันคือผู้ใช้มีความเชี่ยวชาญมากกว่า - นั่นคือRepository<BusinessObject1>และRepository<BusinessObject2>มีหลายประเภทและยิ่งไปกว่านั้นถ้าฉันใช้ a Repository<BusinessObject1>ฉันรู้ว่าฉันจะBusinessObject1กลับไปGetอีก

คุณไม่สามารถเสนอการพิมพ์ที่รัดกุมนี้จากการสืบทอดแบบง่าย คลาสที่เก็บข้อมูลที่เสนอของคุณไม่ทำอะไรเลยเพื่อป้องกันไม่ให้ผู้คนสับสนที่เก็บข้อมูลสำหรับวัตถุธุรกิจประเภทต่าง ๆ หรือรับประกันวัตถุธุรกิจที่ถูกต้องออกมา


ขอบคุณที่ทำให้รู้สึก แต่จุดรวมของการใช้คุณลักษณะภาษาที่ได้รับการยกย่องอย่างสูงนั้นง่ายเหมือนการช่วยเหลือผู้ใช้ที่ปิด IntelliSense หรือไม่? (ฉันพูดเกินจริงไปนิดหน่อย แต่ฉันแน่ใจว่าคุณจะได้คะแนน)
jungle_mole

@zloidooraque: IntelliSense ยังไม่รู้ว่ามีการเก็บวัตถุชนิดใดในที่เก็บ แต่ใช่คุณสามารถทำสิ่งใดโดยไม่ใช้ยาสามัญถ้าคุณเต็มใจที่จะใช้การปลดเปลื้องแทน
gexicide

@ ยาฆ่าเชื้อที่เป็นจุด: ฉันไม่เห็นที่ฉันต้องใช้บรรยากาศถ้าฉันใช้อินเตอร์เฟซทั่วไป ฉันไม่เคยพูดว่า "ใช้Object" นอกจากนี้ฉันเข้าใจว่าทำไมต้องใช้ยาชื่อสามัญเมื่อเขียนคอลเลกชัน (หลักการ DRY) น่าจะเป็นคำถามแรกของฉันควรจะได้รับบางสิ่งบางอย่างเกี่ยวกับการใช้ยาชื่อสามัญนอกบริบทคอลเลกชัน ..
jungle_mole

@zloidooraque: ไม่มีส่วนเกี่ยวข้องกับสภาพแวดล้อม Intellisense ไม่สามารถบอกคุณถ้าIBusinessObjectเป็นหรือBusinessObject1 BusinessObject2ไม่สามารถแก้ไขปัญหาการโอเวอร์โหลดตามประเภทที่ได้มาซึ่งไม่ทราบ มันไม่สามารถปฏิเสธรหัสที่ส่งผ่านผิดประเภท มีการพิมพ์ที่แข็งแกร่งกว่าหนึ่งล้านบิตที่ Intellisense ไม่สามารถทำอะไรได้เลย การสนับสนุนการใช้เครื่องมือที่ดีกว่านั้นเป็นประโยชน์ที่ดี แต่จริงๆแล้วไม่มีอะไรเกี่ยวข้องกับเหตุผลหลัก
DeadMG

@DeadMG และนั่นคือจุดของฉัน: Intellisense ไม่สามารถทำได้: ใช้งานทั่วไปดังนั้นมันจะทำได้หรือไม่ มันสำคัญไหม เมื่อคุณได้รับวัตถุโดยอินเทอร์เฟซทำไม downcast มัน ถ้าคุณทำมันออกแบบไม่ดีไม่? และทำไมและอะไรคือ "แก้ไขโอเวอร์โหลด" ผู้ใช้ต้องไม่ตัดสินใจไม่ว่าจะเป็นวิธีการโทรหรือไม่ขึ้นอยู่กับประเภทที่ได้รับหากเขามอบหมายวิธีการโทรที่ถูกต้องให้กับระบบ (ซึ่งความหลากหลายคือ) และสิ่งนี้ทำให้ฉันคำถาม: generics มีประโยชน์นอกภาชนะบรรจุหรือไม่ ฉันไม่ได้โต้เถียงกับคุณฉันต้องเข้าใจมันจริงๆ
jungle_mole

13

"ลูกค้าที่มีแนวโน้มมากที่สุดของพื้นที่เก็บข้อมูลนี้จะต้องการรับและใช้วัตถุผ่านส่วนต่อประสาน IBusinessObject"

ไม่พวกเขาจะไม่

ลองพิจารณาว่า IBusinessObject มีคำจำกัดความต่อไปนี้:

public interface IBusinessObject
{
  public int Id { get; }
}

มันเป็นเพียงแค่กำหนดรหัสเพราะมันเป็นเพียงฟังก์ชั่นที่ใช้ร่วมกันระหว่างวัตถุธุรกิจทั้งหมด และคุณมีวัตถุทางธุรกิจจริงสองรายการ: บุคคลและที่อยู่ (เนื่องจากผู้คนไม่มีถนนและที่อยู่ไม่มีชื่อคุณไม่สามารถบังคับทั้งสองอย่างนี้ให้กับส่วนต่อประสานที่มี funcionality ของทั้งสองนั่นจะเป็นการออกแบบที่แย่มากหลักการแบ่งส่วนอินเทอร์เฟซ "I" ในSOLID )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

ตอนนี้จะเกิดอะไรขึ้นเมื่อคุณใช้ที่เก็บเวอร์ชันทั่วไป:

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

เมื่อคุณเรียกใช้เมธอด Get บนที่เก็บทั่วไปอ็อบเจ็กต์ที่ส่งคืนจะถูกพิมพ์อย่างมากทำให้คุณเข้าถึงสมาชิกคลาสทั้งหมดได้

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

ในทางกลับกันเมื่อคุณใช้ที่เก็บที่ไม่ใช่ทั่วไป:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

คุณจะสามารถเข้าถึงสมาชิกได้จากอินเทอร์เฟซ IBusinessObject เท่านั้น:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

ดังนั้นรหัสก่อนหน้านี้จะไม่รวบรวมเนื่องจากบรรทัดต่อไปนี้:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

แน่นอนว่าคุณสามารถใช้ IBussinesObject ให้กับคลาสจริงได้ แต่คุณจะสูญเสียเวทย์มนตร์เวลารวบรวมทั้งหมดที่ generics อนุญาต (นำไปสู่ ​​InvalidCastExceptions ตามถนน) จะประสบกับค่าใช้จ่ายที่ไม่จำเป็น ... และแม้ว่าคุณจะไม่ ใส่ใจกับการรวบรวมเวลาตรวจสอบประสิทธิภาพ (คุณควร) การคัดเลือกหลังจากนั้นจะไม่ให้ประโยชน์ใด ๆ แก่คุณมากกว่าการใช้ยาชื่อสามัญในตอนแรก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.