แปลงรายการ <DerivedClass> เป็น List <BaseClass>


179

ในขณะที่เราสามารถสืบทอดจากคลาสพื้นฐาน / อินเทอร์เฟซทำไมเราไม่ประกาศList<> โดยใช้คลาส / อินเทอร์เฟซเดียวกัน

interface A
{ }

class B : A
{ }

class C : B
{ }

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        List<A> listOfA = new List<C>(); // compiler Error
    }
}

มีวิธีแก้ไขไหม?


คำตอบ:


230

วิธีที่จะทำให้งานนี้คือการทำซ้ำในรายการและองค์ประกอบ สามารถทำได้โดยใช้ ConvertAll:

List<A> listOfA = new List<C>().ConvertAll(x => (A)x);

คุณสามารถใช้ Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();

2
ตัวเลือกอื่น: รายการ <A> listOfA = listOfC.ConvertAll (x => (A) x);
ahaliav fox

6
อันไหนเร็วกว่ากัน? ConvertAllหรือCast?
Martin Braun

24
สิ่งนี้จะสร้างสำเนาของรายการ หากคุณเพิ่มหรือลบบางสิ่งในรายการใหม่สิ่งนี้จะไม่ปรากฏในรายการเดิม และประการที่สองมีประสิทธิภาพที่ยอดเยี่ยมและการลงโทษหน่วยความจำเพราะมันสร้างรายการใหม่ที่มีวัตถุที่มีอยู่ ดูคำตอบของฉันด้านล่างสำหรับวิธีการแก้ปัญหาโดยไม่มีปัญหาเหล่านี้
Bigjim

Answer to modiX: ConvertAll เป็นวิธีการในรายการ <T> ดังนั้นมันจะทำงานเฉพาะกับรายการทั่วไป มันจะไม่ทำงานบน IEnumerable หรือ IEnumerable <T>; ConvertAll ยังสามารถทำการแปลงที่กำหนดเองไม่เพียงแค่แคสต์เท่านั้นเช่น ConvertAll (inches => inches * 25.4) Cast <A> เป็นวิธีการขยาย LINQ ดังนั้นทำงานบน IEnumerable <T> ใด ๆ (และยังใช้งานได้กับ IEnumerable ที่ไม่ใช่แบบทั่วไป) และเช่นเดียวกับส่วนใหญ่ของ LINQ ที่ใช้การประมวลผลแบบเลื่อนเวลานั่นคือจะแปลงเฉพาะรายการจำนวนมาก ดึง อ่านเพิ่มเติมได้ที่นี่: codeblog.jonskeet.uk/2011/01/13/…
Edward

172

ก่อนอื่นให้หยุดใช้ชื่อชั้นที่ไม่สามารถเข้าใจได้เช่น A, B, C ใช้สัตว์เลี้ยงลูกด้วยนมยีราฟหรืออาหารผลไม้ส้มหรืออะไรที่ความสัมพันธ์ชัดเจน

คำถามของคุณคือ "ทำไมฉันไม่สามารถกำหนดรายชื่อยีราฟให้กับตัวแปรประเภทสัตว์เนื่องจากฉันสามารถกำหนดยีราฟให้กับตัวแปรประเภทสัตว์ได้"

คำตอบคือ: สมมติว่าคุณทำได้ มีอะไรผิดพลาด?

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

เราเลือกหลัง

การแปลงแบบนี้เรียกว่าการแปลง "covariant" ใน C # 4 เราจะช่วยให้คุณสามารถแปลง covariant บนอินเตอร์เฟซและผู้แทนเมื่อแปลงเป็นที่รู้จักกันจะปลอดภัยเสมอ ดูบทความบล็อกของฉันเกี่ยวกับความแปรปรวนร่วมและความแปรปรวนสำหรับรายละเอียด (จะมีหัวข้อใหม่ในหัวข้อนี้ทั้งวันจันทร์และวันพฤหัสบดีของสัปดาห์นี้)


3
จะมีสิ่งใดที่ไม่ปลอดภัยเกี่ยวกับ IList <T> หรือ ICollection <T> การนำ IList ที่ไม่ใช่แบบทั่วไปมาใช้หรือไม่ แต่การดำเนินการให้มี Ilist / ICollection ที่ไม่ใช่แบบสามัญกลับมาเป็น True สำหรับ IsReadOnly และโยน NotSupportedException สำหรับวิธีการหรือคุณสมบัติใด ๆ
supercat

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

2
@EricLippert ทำไมเราสามารถทำการแปลงนี้โดยใช้IEnumerableแทนList? ie: List<Animal> listAnimals = listGiraffes as List<Animal>;เป็นไปไม่ได้ แต่IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>ทำงานได้
LINQ

3
@jbueno: อ่านย่อหน้าสุดท้ายของคำตอบของฉัน การแปลงมีที่รู้จักกันเพื่อความปลอดภัย ทำไม? เพราะมันเป็นไปไม่ได้ที่จะหันลำดับของยีราฟลงในลำดับของสัตว์และจากนั้นใส่เสือเป็นลำดับของสัตว์ IEnumerable<T>และIEnumerator<T>ทั้งสองทำเครื่องหมายว่าปลอดภัยสำหรับความแปรปรวนร่วมและคอมไพเลอร์ได้ตรวจสอบแล้วว่า
Eric Lippert

60

เพื่ออ้างอิงคำอธิบายที่ดีของ Eric

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

แต่ถ้าคุณต้องการเลือกสำหรับ runtime crash แทนที่จะเป็นคอมไพล์ error? โดยปกติคุณจะใช้ Cast <> หรือ ConvertAll <> แต่คุณจะมีปัญหา 2 ประการ: มันจะสร้างสำเนาของรายการ หากคุณเพิ่มหรือลบบางสิ่งในรายการใหม่สิ่งนี้จะไม่ปรากฏในรายการเดิม และประการที่สองมีประสิทธิภาพที่ยอดเยี่ยมและการลงโทษหน่วยความจำเพราะมันสร้างรายการใหม่ที่มีวัตถุที่มีอยู่

ฉันมีปัญหาเดียวกันและดังนั้นฉันจึงสร้างคลาส wrapper ที่สามารถส่งรายการทั่วไปโดยไม่ต้องสร้างรายการใหม่ทั้งหมด

ในคำถามเดิมคุณสามารถใช้:

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        IList<A> listOfA = new List<C>().CastList<C,A>(); // now ok!
    }
}

และที่นี่คลาส wrapper (+ วิธีการขยาย CastList เพื่อใช้งานง่าย)

public class CastedList<TTo, TFrom> : IList<TTo>
{
    public IList<TFrom> BaseList;

    public CastedList(IList<TFrom> baseList)
    {
        BaseList = baseList;
    }

    // IEnumerable
    IEnumerator IEnumerable.GetEnumerator() { return BaseList.GetEnumerator(); }

    // IEnumerable<>
    public IEnumerator<TTo> GetEnumerator() { return new CastedEnumerator<TTo, TFrom>(BaseList.GetEnumerator()); }

    // ICollection
    public int Count { get { return BaseList.Count; } }
    public bool IsReadOnly { get { return BaseList.IsReadOnly; } }
    public void Add(TTo item) { BaseList.Add((TFrom)(object)item); }
    public void Clear() { BaseList.Clear(); }
    public bool Contains(TTo item) { return BaseList.Contains((TFrom)(object)item); }
    public void CopyTo(TTo[] array, int arrayIndex) { BaseList.CopyTo((TFrom[])(object)array, arrayIndex); }
    public bool Remove(TTo item) { return BaseList.Remove((TFrom)(object)item); }

    // IList
    public TTo this[int index]
    {
        get { return (TTo)(object)BaseList[index]; }
        set { BaseList[index] = (TFrom)(object)value; }
    }

    public int IndexOf(TTo item) { return BaseList.IndexOf((TFrom)(object)item); }
    public void Insert(int index, TTo item) { BaseList.Insert(index, (TFrom)(object)item); }
    public void RemoveAt(int index) { BaseList.RemoveAt(index); }
}

public class CastedEnumerator<TTo, TFrom> : IEnumerator<TTo>
{
    public IEnumerator<TFrom> BaseEnumerator;

    public CastedEnumerator(IEnumerator<TFrom> baseEnumerator)
    {
        BaseEnumerator = baseEnumerator;
    }

    // IDisposable
    public void Dispose() { BaseEnumerator.Dispose(); }

    // IEnumerator
    object IEnumerator.Current { get { return BaseEnumerator.Current; } }
    public bool MoveNext() { return BaseEnumerator.MoveNext(); }
    public void Reset() { BaseEnumerator.Reset(); }

    // IEnumerator<>
    public TTo Current { get { return (TTo)(object)BaseEnumerator.Current; } }
}

public static class ListExtensions
{
    public static IList<TTo> CastList<TFrom, TTo>(this IList<TFrom> list)
    {
        return new CastedList<TTo, TFrom>(list);
    }
}

1
ฉันเพิ่งใช้มันเป็นแบบจำลองมุมมอง MVC และได้มุมมองที่เป็นสากลที่ดีบางส่วน ความคิดที่น่าทึ่ง! ฉันดีใจมากที่ฉันได้อ่านสิ่งนี้
Stefan Cebulak

5
ฉันจะเพิ่ม "โดยที่ TTo: TFrom" ที่การประกาศคลาสดังนั้นคอมไพเลอร์สามารถเตือนการใช้งานที่ไม่ถูกต้อง การสร้าง CastedList ประเภทที่ไม่เกี่ยวข้องนั้นเป็นเรื่องไร้สาระอยู่แล้วและการทำให้ "CastedList <TBase, TDerived>" ไม่มีประโยชน์: คุณไม่สามารถเพิ่มวัตถุ TBase ปกติลงไปได้และ TDerived ใด ๆ ที่คุณได้รับจากรายการดั้งเดิมสามารถใช้ TBase
Wolfzoon

2
@ PaulColdrey อนิจจาเริ่มหกปีคือการตำหนิ
Wolfzoon

@ Wolfzoon: มาตรฐาน. คาสต์ <T> () ไม่ได้มีข้อ จำกัด เช่นกัน ฉันต้องการทำให้พฤติกรรมเหมือนกับคาสต์ดังนั้นจึงเป็นไปได้ที่จะส่งรายการสัตว์ไปยังรายการของเสือและมีข้อยกเว้นเมื่อมันมียีราฟเป็นตัวอย่าง เช่นเดียวกับที่มันจะมีนักแสดง ...
Bigjim

สวัสดี @Bigjim ฉันมีปัญหาในการทำความเข้าใจวิธีใช้คลาส wrapper ของคุณในการหล่ออาร์เรย์ Giraffes ที่มีอยู่ให้เป็นอาร์เรย์ของสัตว์ (คลาสฐาน) เคล็ดลับใดจะ appriciated :)
เดนิส Vitez

28

หากคุณใช้IEnumerableแทนจะสามารถใช้งานได้ (อย่างน้อยใน C # 4.0 ฉันไม่ได้ลองรุ่นก่อนหน้านี้) นี่เป็นเพียงนักแสดงแน่นอนมันจะยังคงเป็นรายการ

แทน -

List<A> listOfA = new List<C>(); // compiler Error

ในรหัสดั้งเดิมของคำถามให้ใช้ -

IEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)


วิธีการใช้ IEnumerable ในกรณีนั้นอย่างแน่นอน?
Vladius

1
แทนที่จะเป็นList<A> listOfA = new List<C>(); // compiler Errorในรหัสดั้งเดิมของคำถามให้ป้อนIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK

วิธีการใช้ IEnumerable <BaseClass> เป็นพารามิเตอร์จะอนุญาตให้คลาสที่สืบทอดมาถูกส่งผ่านเป็นรายการ มีน้อยมากที่ต้องทำ แต่เปลี่ยนประเภทพารามิเตอร์
beauXjames

27

เท่าที่ทำไมมันไม่ทำงานมันอาจจะเป็นประโยชน์ในการเข้าใจ ความแปรปรวนร่วมและความแตกต่าง

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

void DoesThisWork()
{
     List<C> DerivedList = new List<C>();
     List<A> BaseList = DerivedList;
     BaseList.Add(new B());

     C FirstItem = DerivedList.First();
}

ควรใช้งานได้หรือไม่ รายการแรกในรายการเป็นประเภท "B" แต่ประเภทของรายการ DerivedList คือ C

ทีนี้สมมติว่าเราแค่ต้องการสร้างฟังก์ชั่นทั่วไปที่ทำงานในรายการประเภทที่ใช้ A แต่เราไม่สนใจประเภทที่:

void ThisWorks<T>(List<T> GenericList) where T:A
{

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}

"ควรใช้งานได้หรือไม่" - ฉันจะใช่และไม่ใช่ ฉันไม่เห็นเหตุผลใด ๆ ที่คุณไม่ควรเขียนโค้ดและล้มเหลวในเวลารวบรวมเพราะจริง ๆ แล้วมันทำการแปลงที่ไม่ถูกต้องในเวลาที่คุณเข้าถึง FirstItem เป็นประเภท 'C' มีหลายวิธีที่คล้ายคลึงกันในการระเบิด C # ที่รองรับ หากคุณต้องการบรรลุฟังก์ชั่นนี้ด้วยเหตุผลที่ดี (และมีจำนวนมาก) คำตอบของ bigjim ด้านล่างนั้นยอดเยี่ยม
พอล Coldrey

16

คุณสามารถส่งไปยังรายการแบบอ่านอย่างเดียวเท่านั้น ตัวอย่างเช่น:

IEnumerable<A> enumOfA = new List<C>();//This works
IReadOnlyCollection<A> ro_colOfA = new List<C>();//This works
IReadOnlyList<A> ro_listOfA = new List<C>();//This works

และคุณไม่สามารถทำรายการที่สนับสนุนองค์ประกอบการบันทึกได้ เหตุผลคือ:

List<string> listString=new List<string>();
List<object> listObject=(List<object>)listString;//Assume that this is possible
listObject.Add(new object());

เกิดอะไรขึ้น โปรดจำไว้ว่า listObject และ listString เป็นรายการเดียวกันจริง ๆ ดังนั้น listString จึงมีองค์ประกอบวัตถุ - ไม่ควรเป็นไปได้และไม่ใช่


1
อันนี้เป็นคำตอบที่เข้าใจได้มากที่สุดสำหรับฉัน พิเศษเพราะมันระบุว่ามีบางสิ่งที่เรียกว่า IReadOnlyList ซึ่งจะทำงานได้เพราะรับประกันว่าจะไม่มีการเพิ่มองค์ประกอบอีก
bendtherules

มันเป็นความอัปยศที่คุณไม่สามารถทำแบบเดียวกันกับ IReadOnlyDictionary <TKey, TValue> ทำไมถึงเป็นอย่างนั้น?
Alastair Maw

นี่คือข้อเสนอแนะที่ดี ฉันใช้ List <> ทุกที่เพื่อความสะดวก แต่ส่วนใหญ่ฉันต้องการให้อ่านได้อย่างเดียว คำแนะนำที่ดีเช่นนี้จะช่วยปรับปรุงโค้ดของฉันได้ 2 วิธีด้วยการอนุญาตให้นักแสดงนี้และปรับปรุงที่ฉันระบุไว้แบบอ่านอย่างเดียว
Rick Love

1

ฉันชอบที่จะสร้าง libs ด้วยส่วนขยายในชั้นเรียน

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}

0

เนื่องจาก C # ไม่อนุญาตให้ใช้ประเภทนั้น มรดกการแปลงในขณะนี้


6
อย่างแรกนี่เป็นคำถามของการเปลี่ยนแปลงได้ไม่ใช่การสืบทอด ประการที่สองความแปรปรวนร่วมของประเภททั่วไปจะไม่ทำงานกับประเภทชั้นเรียนเฉพาะในส่วนต่อประสานและประเภทตัวแทน
Eric Lippert

5
ฉันก็เถียงกับคุณไม่ได้
Noon Silk

0

นี้เป็นส่วนขยายไปยังสดใส BigJim ของคำตอบ

ในกรณีของฉันฉันมีNodeBaseชั้นเรียนพร้อมChildrenพจนานุกรมและฉันต้องการวิธีการค้นหา O (1) จากเด็ก ๆ โดยทั่วไป ฉันพยายามคืนฟิลด์พจนานุกรมส่วนตัวในทะเยอทะยานของChildrenดังนั้นเห็นได้ชัดว่าฉันต้องการหลีกเลี่ยงการคัดลอก / ทำซ้ำราคาแพง ดังนั้นฉันจึงใช้โค้ดของ Bigjim เพื่อส่งรหัสDictionary<whatever specific type>ทั่วไปDictionary<NodeBase>:

// Abstract parent class
public abstract class NodeBase
{
    public abstract IDictionary<string, NodeBase> Children { get; }
    ...
}

// Implementing child class
public class RealNode : NodeBase
{
    private Dictionary<string, RealNode> containedNodes;

    public override IDictionary<string, NodeBase> Children
    {
        // Using a modification of Bigjim's code to cast the Dictionary:
        return new IDictionary<string, NodeBase>().CastDictionary<string, RealNode, NodeBase>();
    }
    ...
}

มันใช้งานได้ดี อย่างไรก็ตามในที่สุดฉันก็พบข้อ จำกัด ที่ไม่เกี่ยวข้องและจบลงด้วยการสร้างFindChild()วิธีนามธรรมในคลาสฐานที่จะทำการค้นหาแทน เมื่อมันปรากฏออกมาสิ่งนี้ทำให้ความจำเป็นในการใช้พจนานุกรมฉบับแรก (ฉันสามารถแทนที่ด้วยวิธีง่าย ๆIEnumerableสำหรับวัตถุประสงค์ของฉัน)

ดังนั้นคำถามที่คุณอาจถาม (โดยเฉพาะอย่างยิ่งหากประสิทธิภาพเป็นปัญหาที่ห้ามมิให้คุณใช้.Cast<>หรือ.ConvertAll<>) คือ:

"ฉันจำเป็นต้องใช้การรวบรวมทั้งหมดหรือไม่ฉันสามารถใช้วิธีนามธรรมเพื่อเก็บความรู้พิเศษที่จำเป็นในการปฏิบัติงานและหลีกเลี่ยงการเข้าถึงชุดสะสมโดยตรงหรือไม่"

บางครั้งทางออกที่ง่ายที่สุดก็ดีที่สุด


0

คุณยังสามารถใช้System.Runtime.CompilerServices.Unsafeแพ็คเกจ NuGet เพื่อสร้างการอ้างอิงในสิ่งเดียวกันList:

using System.Runtime.CompilerServices;
...
class Tool { }
class Hammer : Tool { }
...
var hammers = new List<Hammer>();
...
var tools = Unsafe.As<List<Tool>>(hammers);

รับตัวอย่างข้างต้นคุณสามารถเข้าถึงHammerอินสแตนซ์ที่มีอยู่ในรายการโดยใช้toolsตัวแปร เพิ่มToolอินสแตนซ์ในรายการพ่นArrayTypeMismatchExceptionข้อยกเว้นเพราะอ้างอิงตัวแปรเดียวกับtoolshammers


0

ฉันได้อ่านหัวข้อทั้งหมดและฉันต้องการชี้ให้เห็นสิ่งที่ดูเหมือนไม่สอดคล้องกับฉัน

คอมไพเลอร์ป้องกันไม่ให้คุณทำการกำหนดกับรายการ:

List<Tiger> myTigersList = new List<Tiger>() { new Tiger(), new Tiger(), new Tiger() };
List<Animal> myAnimalsList = myTigersList;    // Compiler error

แต่คอมไพเลอร์ใช้ได้ดีกับอาเรย์:

Tiger[] myTigersArray = new Tiger[3] { new Tiger(), new Tiger(), new Tiger() };
Animal[] myAnimalsArray = myTigersArray;    // No problem

การโต้เถียงกันว่างานที่ได้รับมอบหมายนั้นปลอดภัยหรือไม่ การมอบหมายผมกับอาร์เรย์จะไม่ปลอดภัย เพื่อพิสูจน์ว่าถ้าฉันทำตามนั้น:

myAnimalsArray[1] = new Giraffe();

ฉันได้รับข้อยกเว้นรันไทม์ "ArrayTypeMismatchException" เราอธิบายสิ่งนี้ได้อย่างไร หากคอมไพเลอร์ต้องการที่จะป้องกันไม่ให้ฉันทำอะไรที่โง่มันควรจะป้องกันไม่ให้ฉันทำการมอบหมายอาเรย์

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