Chuyển đổi danh sách <DeruredClass> thành Danh sách <BaseClass>


179

Mặc dù chúng ta có thể kế thừa từ lớp / giao diện cơ sở, tại sao chúng ta không thể khai báo một List<> lớp / giao diện giống nhau?

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
    }
}

Có cách nào xung quanh không?


Câu trả lời:


230

Cách để thực hiện công việc này là lặp lại danh sách và bỏ qua các phần tử. Điều này có thể được thực hiện bằng ConvertAll:

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

Bạn cũng có thể sử dụng Linq:

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

2
tùy chọn khác: Danh sách <A> listOfA = listOfC.Convert ALL (x => (A) x);
cáo ahaliav

6
Cái nào nhanh hơn? ConvertAllhay Cast?
Martin Braun

24
Điều này sẽ tạo ra một bản sao của danh sách. Nếu bạn thêm hoặc xóa một cái gì đó trong danh sách mới, điều này sẽ không được phản ánh trong danh sách ban đầu. Và thứ hai, có một hình phạt hiệu năng và bộ nhớ lớn vì nó tạo ra một danh sách mới với các đối tượng hiện có. Xem câu trả lời của tôi dưới đây cho một giải pháp mà không có những vấn đề này.
Bigjim

Trả lời cho modiX: ConvertAll là một phương thức trong Danh sách <T>, vì vậy nó sẽ chỉ hoạt động trên một danh sách chung; nó sẽ không hoạt động trên IEnumerable hoặc IEnumerable <T>; ConvertAll cũng có thể thực hiện chuyển đổi tùy chỉnh không chỉ truyền, ví dụ ConvertAll (inch => inch * 25.4). Truyền <A> là một phương thức mở rộng LINQ, do đó, hoạt động trên mọi IEnumerable <T> (và cũng hoạt động cho IEnumerable không chung chung), và giống như hầu hết LINQ, nó sử dụng thực thi hoãn lại, nghĩa là chỉ chuyển đổi nhiều mục như lấy lại. Đọc thêm về nó ở đây: codeblog.jonskeet.uk/2011/01/13/ Khăn
Edward

172

Trước hết, ngừng sử dụng các tên lớp không thể hiểu như A, B, C. Sử dụng Động vật, Động vật có vú, Hươu cao cổ, hoặc Thực phẩm, Trái cây, Cam hoặc một cái gì đó mà mối quan hệ rõ ràng.

Câu hỏi của bạn là "tại sao tôi không thể gán danh sách hươu cao cổ cho một biến danh sách loại động vật, vì tôi có thể gán hươu cao cổ cho một loại động vật?"

Câu trả lời là: giả sử bạn có thể. Điều gì sau đó có thể đi sai?

Chà, bạn có thể thêm một con hổ vào danh sách các loài động vật. Giả sử chúng tôi cho phép bạn đặt một danh sách hươu cao cổ trong một biến chứa danh sách động vật. Sau đó, bạn cố gắng thêm một con hổ vào danh sách đó. Chuyện gì xảy ra Bạn có muốn danh sách hươu cao cổ có chứa một con hổ? Bạn có muốn một vụ tai nạn? hoặc bạn muốn trình biên dịch bảo vệ bạn khỏi sự cố bằng cách chuyển nhượng bất hợp pháp ngay từ đầu?

Chúng tôi chọn cái sau.

Loại chuyển đổi này được gọi là chuyển đổi "covariant". Trong C # 4, chúng tôi sẽ cho phép bạn thực hiện chuyển đổi đồng biến trên giao diện và đại biểu khi chuyển đổi được biết là luôn an toàn . Xem các bài viết trên blog của tôi về hiệp phương sai và chống chỉ định để biết chi tiết. (Sẽ có một cái mới về chủ đề này vào cả Thứ Hai và Thứ Năm của tuần này.)


3
Sẽ có bất cứ điều gì không an toàn về một IList <T> hoặc ICollection <T> triển khai IList không chung chung, nhưng thực hiện có trả về Ilist / ICollection không chung chung True cho IsReadOnly và ném NotSupportedException cho bất kỳ phương thức hoặc thuộc tính nào sẽ sửa đổi nó?
supercat

33
Trong khi câu trả lời này chứa lý do khá chấp nhận được thì nó không thực sự "đúng". Câu trả lời đơn giản là C # không hỗ trợ điều này. Ý tưởng có một danh sách các động vật có chứa hươu cao cổ và hổ là hoàn toàn hợp lệ. Vấn đề duy nhất đến khi bạn muốn truy cập các lớp cấp cao hơn. Trong thực tế, điều này không khác gì việc chuyển một lớp cha làm tham số cho một hàm và sau đó cố gắng chuyển nó sang một lớp anh chị em khác. Có thể có vấn đề kỹ thuật với việc thực hiện dàn diễn viên nhưng lời giải thích ở trên không cung cấp bất kỳ lý do nào khiến nó trở thành một ý tưởng tồi.
Paul Coldrey

2
@EricLippert Tại sao chúng ta có thể thực hiện chuyển đổi này bằng cách sử dụng IEnumerablethay vì List? tức là: List<Animal> listAnimals = listGiraffes as List<Animal>;không thể, nhưng IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>hoạt động.
LINQ

3
@jbueno: Đọc đoạn cuối câu trả lời của tôi. Việc chuyển đổi được biết là an toàn . Tại sao? Bởi vì không thể biến một chuỗi hươu cao cổ thành một chuỗi động vật và sau đó đưa một con hổ vào chuỗi động vật . IEnumerable<T>IEnumerator<T>cả hai đều được đánh dấu là an toàn cho hiệp phương sai và trình biên dịch đã xác minh điều đó.
Eric Lippert

60

Trích dẫn lời giải thích tuyệt vời của Eric

Chuyện gì xảy ra Bạn có muốn danh sách hươu cao cổ có chứa một con hổ? Bạn có muốn một vụ tai nạn? hoặc bạn muốn trình biên dịch bảo vệ bạn khỏi sự cố bằng cách chuyển nhượng bất hợp pháp ngay từ đầu? Chúng tôi chọn cái sau.

Nhưng nếu bạn muốn chọn cho một sự cố thời gian chạy thay vì một lỗi biên dịch thì sao? Thông thường bạn sẽ sử dụng Cast <> hoặc Convert ALL <> nhưng sau đó bạn sẽ gặp 2 vấn đề: Nó sẽ tạo một bản sao của danh sách. Nếu bạn thêm hoặc xóa một cái gì đó trong danh sách mới, điều này sẽ không được phản ánh trong danh sách ban đầu. Và thứ hai, có một hình phạt hiệu năng và bộ nhớ lớn vì nó tạo ra một danh sách mới với các đối tượng hiện có.

Tôi có cùng một vấn đề và do đó tôi đã tạo ra một lớp bao bọc có thể tạo một danh sách chung mà không tạo ra một danh sách hoàn toàn mới.

Trong câu hỏi ban đầu bạn có thể sử dụng:

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

và ở đây lớp trình bao bọc (+ một phương thức mở rộng CastList để dễ sử dụng)

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
Tôi vừa mới sử dụng nó làm mô hình khung nhìn MVC và có chế độ xem một phần dao cạo phổ quát. Ý tưởng tuyệt vời! Tôi rất vui vì tôi đã đọc nó.
Stefan Cebulak

5
Tôi sẽ chỉ thêm một "nơi TTo: TFrom" vào khai báo lớp, vì vậy trình biên dịch có thể cảnh báo chống lại việc sử dụng không chính xác. Tạo một CastedList của các loại không liên quan dù sao cũng là vô nghĩa và việc tạo một "CastedList <TBase, TDerured>" sẽ vô dụng: Bạn không thể thêm các đối tượng TBase thông thường vào nó và bất kỳ TDerive nào bạn nhận được từ Danh sách gốc đều có thể được sử dụng như ở mức cơ bản.
Wolfzoon

2
@PaulColdrey Alas, khởi đầu sáu năm là đáng trách.
Wolfzoon

@Wolfzoon: tiêu chuẩn .Cast <T> () cũng không có hạn chế này. Tôi muốn thực hiện hành vi giống như .Cast để có thể đưa danh sách Động vật vào danh sách Hổ, và có một ngoại lệ khi nó có chứa Gi hươu cao cổ chẳng hạn. Giống như với Cast ...
Bigjim

Xin chào @Bigjim, tôi đang gặp vấn đề trong việc hiểu cách sử dụng lớp trình bao bọc của mình, để đưa một mảng Giraffes hiện có vào một mảng Động vật (lớp cơ sở). Bất kỳ lời khuyên nào cũng sẽ được đánh giá :)
Denis Vitez

28

Nếu bạn sử dụng IEnumerablethay thế, nó sẽ hoạt động (ít nhất là trong C # 4.0, tôi chưa thử các phiên bản trước). Đây chỉ là một diễn viên, tất nhiên, nó vẫn sẽ là một danh sách.

Thay vì -

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

Trong mã gốc của câu hỏi, sử dụng -

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


Làm thế nào để sử dụng IEnumerable chính xác trong trường hợp đó?
Vladius

1
Thay vì List<A> listOfA = new List<C>(); // compiler Errortrong mã gốc của câu hỏi, hãy nhậpIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK

Phương thức lấy IEnumerable <BaseClass> làm tham số sẽ cho phép lớp kế thừa được truyền vào dưới dạng Danh sách. Vì vậy, có rất ít để làm nhưng thay đổi loại tham số.
beauXjames

27

Theo như lý do tại sao nó không hoạt động, có thể hữu ích để hiểu hiệp phương sai và chống chỉ định .

Chỉ để cho thấy lý do tại sao điều này không hoạt động, đây là một thay đổi đối với mã bạn đã cung cấp:

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

     C FirstItem = DerivedList.First();
}

Có nên làm việc này? Mục đầu tiên trong danh sách là Loại "B", nhưng loại mục DeriveList là C.

Bây giờ, giả sử rằng chúng ta thực sự chỉ muốn tạo một hàm chung hoạt động trên một danh sách một số loại thực hiện A, nhưng chúng ta không quan tâm đó là loại nào:

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

}

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

"Có nên làm việc này?" - Tôi sẽ có và không. Tôi thấy không có lý do tại sao bạn không thể viết mã và bị lỗi khi biên dịch vì nó thực sự đang thực hiện một chuyển đổi không hợp lệ tại thời điểm bạn truy cập FirstItem dưới dạng 'C'. Có nhiều cách tương tự để làm nổ tung C # được hỗ trợ. NẾU bạn thực sự muốn đạt được chức năng này vì một lý do chính đáng (và có rất nhiều) thì câu trả lời của bigjim dưới đây thật tuyệt vời.
Paul Coldrey

16

Bạn chỉ có thể truyền vào danh sách chỉ đọc. Ví dụ:

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

Và bạn không thể làm điều đó cho các danh sách hỗ trợ các yếu tố tiết kiệm. Lý do là:

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

Gì bây giờ? Hãy nhớ rằng listObject và listString thực sự là cùng một danh sách, vì vậy listString hiện có phần tử đối tượng - điều đó là không thể và không phải vậy.


1
Đây là câu trả lời dễ hiểu nhất đối với tôi. Đặc biệt bởi vì nó đề cập rằng có một cái gì đó gọi là IReadOnlyList, nó sẽ hoạt động vì nó đảm bảo rằng sẽ không có thêm phần tử nào được thêm vào.
uốn cong

Thật đáng tiếc khi bạn không thể làm tương tự cho IReadOnlyDixi <TKey, TValue>. Tại sao vậy?
Alastair Maw

Đây là một gợi ý tuyệt vời. Tôi sử dụng Danh sách <> ở mọi nơi để thuận tiện, nhưng hầu hết thời gian tôi muốn nó được đọc. Đề xuất hay vì điều này sẽ cải thiện mã của tôi theo 2 cách bằng cách cho phép diễn viên này và cải thiện nơi tôi chỉ định chỉ đọc.
Rick Love

1

Cá nhân tôi thích tạo lib với các phần mở rộng cho các lớp

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

0

Bởi vì C # không cho phép loại đó di sảnchuyển đổi tại thời điểm này .


6
Đầu tiên, đây là một câu hỏi về khả năng chuyển đổi, không phải về thừa kế. Thứ hai, hiệp phương sai của các loại chung sẽ không hoạt động trên các loại lớp, chỉ trên các loại giao diện và đại biểu.
Eric Lippert

5
Vâng, tôi khó có thể tranh luận với bạn.
Trưa Silk

0

Đây là một phần mở rộng cho câu trả lời tuyệt vời của BigJim .

Trong trường hợp của tôi, tôi đã có một NodeBaselớp học với một Childrencuốn từ điển và tôi cần một cách tổng quát để tìm kiếm O (1) từ trẻ em. Tôi đã cố gắng trả lại một trường từ điển riêng trong getter của Children, vì vậy rõ ràng tôi muốn tránh sao chép / lặp đắt tiền. Do đó, tôi đã sử dụng mã của Bigjim để Dictionary<whatever specific type>chuyển thành chung chung 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>();
    }
    ...
}

Điều này làm việc tốt. Tuy nhiên, cuối cùng tôi đã gặp phải những hạn chế không liên quan và cuối cùng đã tạo ra một FindChild()phương thức trừu tượng trong lớp cơ sở để thực hiện tra cứu thay thế. Khi nó bật ra điều này đã loại bỏ sự cần thiết cho từ điển đúc ngay từ đầu. (Tôi đã có thể thay thế nó bằng một cách đơn giản IEnumerablecho mục đích của mình.)

Vì vậy, câu hỏi bạn có thể hỏi (đặc biệt nếu hiệu suất là vấn đề cấm bạn sử dụng .Cast<>hoặc .ConvertAll<>) là:

"Tôi có thực sự cần phải đúc toàn bộ bộ sưu tập hay tôi có thể sử dụng một phương pháp trừu tượng để nắm giữ kiến ​​thức đặc biệt cần thiết để thực hiện nhiệm vụ và do đó tránh truy cập trực tiếp vào bộ sưu tập?"

Đôi khi giải pháp đơn giản nhất là tốt nhất.


0

Bạn cũng có thể sử dụng System.Runtime.CompilerServices.Unsafegói NuGet để tạo tham chiếu giống nhau List:

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

Cho mẫu ở trên, bạn có thể truy cập các thể hiện hiện có Hammertrong danh sách bằng cách sử dụng toolsbiến. Việc thêm các Toolthể hiện vào danh sách sẽ ném một ArrayTypeMismatchExceptionngoại lệ vì toolstham chiếu cùng một biến như hammers.


0

Tôi đã đọc toàn bộ chủ đề này, và tôi chỉ muốn chỉ ra những gì có vẻ như không nhất quán với tôi.

Trình biên dịch ngăn bạn thực hiện việc gán với Danh sách:

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

Nhưng trình biên dịch hoàn toàn tốt với các mảng:

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

Cuộc tranh luận về việc liệu nhiệm vụ được biết là an toàn sẽ sụp đổ ở đây. Nhiệm vụ tôi đã làm với mảng không an toàn . Để chứng minh điều đó, nếu tôi làm theo điều đó:

myAnimalsArray[1] = new Giraffe();

Tôi nhận được một ngoại lệ thời gian chạy "ArrayTypeMismatchException". Làm thế nào để một người giải thích điều này? Nếu trình biên dịch thực sự muốn ngăn tôi làm điều gì đó ngu ngốc, thì nó sẽ ngăn tôi thực hiện việc gán mảng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.