C #: Ghi đè các loại trả lại


80

Có cách nào để ghi đè các kiểu trả về trong C # không? Nếu vậy thì làm thế nào, và nếu không thì tại sao và cách làm được khuyến nghị là gì?

Trường hợp của tôi là tôi có một giao diện với một lớp cơ sở trừu tượng và con cháu của nó. Tôi muốn làm điều này (không thực sự, nhưng như một ví dụ!):

public interface Animal
{
   Poo Excrement { get; }
}

public class AnimalBase
{
   public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog
{
  // No override, just return normal poo like normal animal
}

public class Cat
{
  public override RadioactivePoo Excrement { get { return new RadioActivePoo(); } }
}

RadioactivePootất nhiên kế thừa từ Poo.

Lý do của tôi muốn điều này là để những người sử dụng Catcác đối tượng có thể sử dụng Excrementtài sản mà không cần phải Poonhập vào RadioactivePootrong khi ví dụ, Catnó vẫn có thể là một phần của Animaldanh sách mà người dùng có thể không nhất thiết phải biết hoặc quan tâm đến phân phóng xạ của họ. Hy vọng điều đó có ý nghĩa ...

Theo như tôi có thể thấy trình biên dịch không cho phép điều này ít nhất. Vì vậy, tôi đoán nó là không thể. Nhưng những gì bạn sẽ đề xuất như một giải pháp cho điều này?


2
Còn thuốc generic thì sao? Họ sẽ không giúp đỡ?
Arnis Lapsa

3
Không nên Cat.Excrement () chỉ trả về và phiên bản của RadioActivePoo dưới dạng Poo? Bạn có một giao diện chung, hãy sử dụng nó. (Và nhờ những ví dụ cuồng loạn.)
hometoast

1
Tôi cũng vậy, muốn nói lời cảm ơn ví dụ như: @goodgai dưới đây là gợi ý để tạo ra trừu tượng Poo - Tôi tự hỏi làm thế nào tôi có thể giải thích điều này cho một tổ chức phi lập trình viên ...
Paolo Tedesco

1
@ Konrad: Hơi chắc chắn nếu tôi nên chỉ cười nhạo rằng bình luận rực rỡ, hoặc nếu tôi cũng nên tự hỏi nếu nó trên thực tế là một mã mùi rất khủng khiếp đó mà nên được chỉ ra (nguyên nhân trong trường hợp đó tôi muốn biết về nó: p)
Svish

3
Tôi gần như thất vọng về bản thân mình với cách hài hước tôi tìm thấy những ví dụ XD
Gurgadurgen

Câu trả lời:


22

Tôi biết có rất nhiều giải pháp cho vấn đề này nhưng tôi nghĩ tôi đã nghĩ ra một giải pháp khắc phục các vấn đề tôi gặp phải với các giải pháp hiện có.

Tôi không hài lòng với một số giải pháp hiện có vì những lý do sau:

  • Giải pháp đầu tiên của Paolo Tedesco: Mèo và Chó không có một lớp cơ sở chung.
  • Giải pháp thứ hai của Paolo Tedesco: Nó hơi phức tạp và khó đọc.
  • Giải pháp của Daniel Daranas: Điều này hoạt động nhưng nó sẽ làm xáo trộn mã của bạn với rất nhiều câu lệnh truyền và Debug.Assert () không cần thiết.
  • giải pháp của hjb417: Giải pháp này không cho phép bạn giữ logic của mình trong một lớp cơ sở. Logic khá đơn giản trong ví dụ này (gọi một hàm tạo) nhưng trong một ví dụ thực tế thì không.

Giải pháp của tôi

Giải pháp này sẽ khắc phục tất cả các vấn đề mà tôi đã đề cập ở trên bằng cách sử dụng cả generic và phương thức ẩn.

public class Poo { }
public class RadioactivePoo : Poo { }

interface IAnimal
{
    Poo Excrement { get; }
}

public class BaseAnimal<PooType> : IAnimal
    where PooType : Poo, new()
{
    Poo IAnimal.Excrement { get { return (Poo)this.Excrement; } }

    public PooType Excrement
    {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

Với giải pháp này, bạn không cần ghi đè bất kỳ thứ gì trong Dog OR Cat! Đây là một số cách sử dụng mẫu:

Cat bruce = new Cat();
IAnimal bruceAsAnimal = bruce as IAnimal;
Console.WriteLine(bruce.Excrement.ToString());
Console.WriteLine(bruceAsAnimal.Excrement.ToString());

Điều này sẽ xuất ra: "RadioactivePoo" hai lần cho thấy tính đa hình chưa bị phá vỡ.

Đọc thêm

  • Triển khai giao diện rõ ràng
  • Modifier mới . Tôi đã không sử dụng nó trong giải pháp đơn giản này nhưng bạn có thể cần nó trong một giải pháp phức tạp hơn. Ví dụ: nếu bạn muốn tạo một giao diện cho BaseAnimal thì bạn sẽ cần sử dụng nó trong quá trình loại bỏ "PooType Excrement".
  • ra Modifier chung (Covariance) . Một lần nữa, tôi không sử dụng nó trong giải pháp này nhưng nếu bạn muốn làm điều gì đó như trở về MyType<Poo>từ IAnimal và trở về MyType<PooType>từ BaseAnimal thì bạn sẽ cần phải sử dụng nó để có thể truyền giữa cả hai.

6
Anh bạn, điều này có thể rất tuyệt. Tôi không có thời gian để phân tích thêm vào lúc này, nhưng có vẻ như bạn có thể đã hiểu nó, và nếu bạn đã bẻ khóa này, xin chúc mừng và cảm ơn vì đã chia sẻ. Thật không may, việc kinh doanh 'phân' và 'phân' này là một sự phân tâm lớn.
Nicholas Petersen

1
Tôi nghĩ rằng tôi đã tìm thấy giải pháp cho một vấn đề liên quan, trong đó một phương thức phải trả về kiểu kế thừa - tức là một phương thức trong 'Dog' đã được kế thừa từ 'Animal', tuy nhiên trả về Dog (this), không phải Animal. Nó được thực hiện với các phương thức mở rộng, tôi có thể chia sẻ ở đây khi tôi bắt đầu.
Nicholas Petersen

Liên quan đến loại an toàn sẽ có cả bruce.Excement và bruceAsAnimal.Excrement thuộc loại RadioactivePoo ??
Anestis Kivranoglou

@AnestisKivranoglou vâng, cả hai sẽ thuộc loại RadioactivePoo
cướp

Tôi đã cày SO trong 48 giờ, và câu trả lời này vừa bẻ khóa nó. Và tôi giống như ... "Anh bạn, điều này có thể rất tuyệt."
Martin Hansen Lennox

50

Còn về một lớp cơ sở chung thì sao?

public class Poo { }
public class RadioactivePoo : Poo { }

public class BaseAnimal<PooType> 
    where PooType : Poo, new() {
    PooType Excrement {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

CHỈNH SỬA : Một giải pháp mới, sử dụng các phương pháp mở rộng và giao diện đánh dấu ...

public class Poo { }
public class RadioactivePoo : Poo { }

// just a marker interface, to get the poo type
public interface IPooProvider<PooType> { }

// Extension method to get the correct type of excrement
public static class IPooProviderExtension {
    public static PooType StronglyTypedExcrement<PooType>(
        this IPooProvider<PooType> iPooProvider) 
        where PooType : Poo {
        BaseAnimal animal = iPooProvider as BaseAnimal;
        if (null == animal) {
            throw new InvalidArgumentException("iPooProvider must be a BaseAnimal.");
        }
        return (PooType)animal.Excrement;
    }
}

public class BaseAnimal {
    public virtual Poo Excrement {
        get { return new Poo(); }
    }
}

public class Dog : BaseAnimal, IPooProvider<Poo> { }

public class Cat : BaseAnimal, IPooProvider<RadioactivePoo> {
    public override Poo Excrement {
        get { return new RadioactivePoo(); }
    }
}

class Program { 
    static void Main(string[] args) {
        Dog dog = new Dog();
        Poo dogPoo = dog.Excrement;

        Cat cat = new Cat();
        RadioactivePoo catPoo = cat.StronglyTypedExcrement();
    }
}

Bằng cách này Dog và Cat đều kế thừa từ Animal (như đã nhận xét trong các ý kiến, giải pháp đầu tiên của tôi đã không bảo toàn tính kế thừa).
Cần phải đánh dấu rõ ràng các lớp bằng giao diện đánh dấu, điều này thật khó khăn, nhưng có thể điều này có thể cung cấp cho bạn một số ý tưởng ...

CHỈNH SỬA THỨ HAI @Svish: Tôi đã sửa đổi mã để hiển thị rõ ràng rằng phương thức mở rộng không thực thi theo bất kỳ cách nào mà thực tế iPooProviderkế thừa từ đó BaseAnimal. Bạn có nghĩa là gì khi "thậm chí còn được gõ mạnh hơn"?


Chỉ đang nghĩ điều tương tự.
Bác sĩ Jones

9
Bạn không lo lắng về sự phân cực giữa Chó và Mèo?
almog.ori

Điều gì xảy ra nếu bạn cũng muốn ghi đè các loại khác? Về mặt kỹ thuật, bạn có thể kết thúc với một loạt các đối số kiểu. Điều mà tôi nghĩ rằng tôi có thể đã thấy phiền phức ... nhưng vâng, đây là một giải pháp.
Svish

@Svish: Tôi tưởng tượng một khi điều đó xảy ra, đã đến lúc sử dụng khung phụ thuộc.
Brian

Làm cách nào để phương thức StronglyTypedExcrement biết rằng iPooProvider là một BaseAnimal? Nó chỉ đoán? Hay là một thứ mà tôi không nhìn thấy? Có thể làm cho phương thức đó được nhập mạnh hơn không?
Svish 14/07/09

32

Đây được gọi là hiệp phương sai kiểu trả về và không được hỗ trợ trong C # hoặc .NET nói chung, bất chấp mong muốn của một số người .

Những gì tôi sẽ làm là giữ nguyên chữ ký nhưng thêm một ENSUREmệnh đề bổ sung vào lớp dẫn xuất mà tôi đảm bảo rằng cái này trả về a RadioActivePoo. Vì vậy, trong ngắn hạn, tôi sẽ làm thông qua thiết kế theo hợp đồng những gì tôi không thể làm qua cú pháp.

Những người khác thích giả mạo nó thay thế. Tôi đoán là ổn, nhưng tôi có xu hướng tiết kiệm các dòng mã "cơ sở hạ tầng". Nếu ngữ nghĩa của mã đủ rõ ràng, tôi rất vui và thiết kế theo hợp đồng cho phép tôi đạt được điều đó, mặc dù nó không phải là cơ chế thời gian biên dịch.

Tương tự đối với thuốc chung, mà các câu trả lời khác đề xuất. Tôi sẽ sử dụng chúng vì một lý do tốt hơn là chỉ trả lại poo phóng xạ - nhưng đó chỉ là tôi.


Điều khoản ENSURE là gì? Nó sẽ hoạt động như thế nào? Nó có phải là một thuộc tính trong .Net không?
Svish

1
Trong .Net và trước khi xem các Hợp đồng mã của .Net 4.0, tôi viết các mệnh đề ENSURE (x) đơn giản là "Debug.Assert (x)". Đối với tài liệu tham khảo thêm xem ví dụ archive.eiffel.com/doc/manuals/technology/contract/page.html hoặc Object Oriented Software Xây dựng, 2nd Edition, bởi Bertrand Meyer (1994) Chương 11.
Daniel Daranas

5
"Tôi sẽ sử dụng chúng vì một lý do tốt hơn so với chỉ trở về poo phóng xạ - nhưng đó chỉ là tôi" thuộc trong danh sách của tôi về dấu ngoặc kép riêng yêu thích :)
Daniel Daranas

9

Ngoài ra còn có tùy chọn này (triển khai giao diện rõ ràng)

public class Cat:Animal
{
  Poo Animal.Excrement { get { return Excrement; } }
  public RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

Bạn mất khả năng sử dụng lớp cơ sở để triển khai Cat, nhưng về mặt tích cực, bạn giữ được tính đa hình giữa Cat và Dog.

Nhưng tôi nghi ngờ sự phức tạp thêm vào là giá trị nó.


4

Tại sao không xác định một phương thức ảo được bảo vệ để tạo ra 'Bài tiết' và giữ tài sản công trả về 'Bài tiết' không ảo. Sau đó, các lớp dẫn xuất có thể ghi đè kiểu trả về của lớp cơ sở.

Trong ví dụ sau, tôi đặt 'Excrement' không phải là ảo nhưng cung cấp thuộc tính ExcrementImpl để cho phép các lớp dẫn xuất cung cấp 'Poo' thích hợp. Sau đó, các kiểu có nguồn gốc có thể ghi đè kiểu trả về là 'Excrement' bằng cách ẩn việc triển khai lớp cơ sở.

Ví dụ:

namepace ConsoleApplication8

{
public class Poo { }

public class RadioactivePoo : Poo { }

public interface Animal
{
    Poo Excrement { get; }
}

public class AnimalBase
{
    public Poo Excrement { get { return ExcrementImpl; } }

    protected virtual Poo ExcrementImpl
    {
        get { return new Poo(); }
    }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class Cat : AnimalBase
{
    protected override Poo ExcrementImpl
    {
        get { return new RadioactivePoo(); }
    }

    public new RadioactivePoo Excrement { get { return (RadioactivePoo)ExcrementImpl; } }
}
}

ví dụ khiến điều này khó hiểu hơn rất nhiều. nhưng mã tuyệt vời!
rposky 17/02/12

2

Hãy sửa tôi nếu tôi sai nhưng không phải là toàn bộ điểm của tính phân cực để có thể trả lại RadioActivePoo nếu nó kế thừa từ Poo, hợp đồng sẽ giống như lớp trừu tượng nhưng chỉ trả về RadioActivePoo ()


2
Bạn đang hoàn toàn đúng, nhưng ông muốn tránh các diễn viên phụ và một số công trình gõ mạnh hơn, đó là hầu hết những gì Generics là dành cho ...
Paolo Tedesco

2

Thử cái này:

namespace ClassLibrary1
{
    public interface Animal
    {   
        Poo Excrement { get; }
    }

    public class Poo
    {
    }

    public class RadioactivePoo
    {
    }

    public class AnimalBase<T>
    {   
        public virtual T Excrement
        { 
            get { return default(T); } 
        }
    }


    public class Dog : AnimalBase<Poo>
    {  
        // No override, just return normal poo like normal animal
    }

    public class Cat : AnimalBase<RadioactivePoo>
    {  
        public override RadioactivePoo Excrement 
        {
            get { return new RadioactivePoo(); } 
        }
    }
}

Giao diện Động vật ở đây có điểm gì đáng chú ý? Không có gì kế thừa từ nó.
cướp

1

Tôi nghĩ rằng tôi đã tìm ra một cách không phụ thuộc vào generic hoặc các phương thức mở rộng, mà là ẩn phương pháp. Tuy nhiên, nó có thể phá vỡ tính đa hình, vì vậy hãy đặc biệt cẩn thận nếu bạn kế thừa từ Cat.

Tôi hy vọng bài đăng này vẫn có thể giúp được ai đó, mặc dù đã muộn 8 tháng.

public interface Animal
{
    Poo Excrement { get; }
}

public class Poo
{
}

public class RadioActivePoo : Poo
{
}

public class AnimalBase : Animal
{
    public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class CatBase : AnimalBase
{
    public override Poo Excrement { get { return new RadioActivePoo(); } }
}

public class Cat : CatBase
{
    public new RadioActivePoo Excrement { get { return (RadioActivePoo) base.Excrement; } }
}

Aack, đừng bận tâm. Tôi không nhận ra hjb417 đã đăng một giải pháp tương tự. Ít nhất là của tôi không yêu cầu sửa đổi lớp cơ sở.
Cybis

"giải pháp" của bạn KHÔNG phá vỡ tính đa hình, vì vậy nó không thực sự là một giải pháp. Mặt khác, giải pháp hjb là giải pháp thực sự, và IMHO khá thông minh.
greenoldman

0

Sẽ hữu ích nếu RadioactivePoo được bắt nguồn từ poo và sau đó sử dụng thuốc chung.


0

FYI. Điều này được thực hiện khá dễ dàng trong Scala.

trait Path

trait Resource
{
    def copyTo(p: Path): Resource
}
class File extends Resource
{
    override def copyTo(p: Path): File = new File
    override def toString = "File"
}
class Directory extends Resource
{
    override def copyTo(p: Path): Directory = new Directory
    override def toString = "Directory"
}

val test: Resource = new Directory()
test.copyTo(null)

Đây là một ví dụ trực tiếp mà bạn có thể chơi với: http://www.scalakata.com/50d0d6e7e4b0a825d655e832


0

Tôi tin rằng câu trả lời của bạn được gọi là hiệp phương sai.

class Program
{
    public class Poo
    {
        public virtual string Name { get{ return "Poo"; } }
    }

    public class RadioactivePoo : Poo
    {
        public override string Name { get { return "RadioactivePoo"; } }
        public string DecayPeriod { get { return "Long time"; } }
    }

    public interface IAnimal<out T> where T : Poo
    {
        T Excrement { get; }
    }

    public class Animal<T>:IAnimal<T> where T : Poo 
    {
        public T Excrement { get { return _excrement ?? (_excrement = (T) Activator.CreateInstance(typeof (T), new object[] {})); } } 
        private T _excrement;
    }

    public class Dog : Animal<Poo>{}
    public class Cat : Animal<RadioactivePoo>{}

    static void Main(string[] args)
    {
        var dog = new Dog();
        var cat = new Cat();

        IAnimal<Poo> animal1 = dog;
        IAnimal<Poo> animal2 = cat;

        Poo dogPoo = dog.Excrement;
        //RadioactivePoo dogPoo2 = dog.Excrement; // Error, dog poo is not RadioactivePoo.

        Poo catPoo = cat.Excrement;
        RadioactivePoo catPoo2 = cat.Excrement;

        Poo animal1Poo = animal1.Excrement;
        Poo animal2Poo = animal2.Excrement;
        //RadioactivePoo animal2RadioactivePoo = animal2.Excrement; // Error, IAnimal<Poo> reference do not know better.


        Console.WriteLine("Dog poo name: {0}",dogPoo.Name);
        Console.WriteLine("Cat poo name: {0}, decay period: {1}" ,catPoo.Name, catPoo2.DecayPeriod);
        Console.WriteLine("Press any key");

        var key = Console.ReadKey();
    }
}

0

Bạn chỉ có thể sử dụng một Giao diện trả về. Trong trường hợp của bạn, IPoo.

Điều này tốt hơn là sử dụng một kiểu chung, trong trường hợp của bạn, vì bạn đang sử dụng một lớp cơ sở nhận xét.


0

Phần sau kết hợp một số khía cạnh tốt nhất của một số câu trả lời khác cũng như một kỹ thuật để cho phép khía cạnh quan trọng của Catviệc có Excrementthuộc tính thuộc RadioactivePooloại bắt buộc , nhưng có thể trả lại điều đó như Poothể chúng ta chỉ biết rằng chúng ta có một thuộc tính AnimalBasehơn là cụ thể a Cat.

Người gọi không bắt buộc phải sử dụng các hàm chung, ngay cả khi chúng có mặt trong các triển khai, cũng như không gọi một hàm có tên khác để trở nên đặc biệt Poo.

Lớp trung gian AnimalWithSpecialisationschỉ phục vụ để niêm phong thuộc Excrementtính, kết nối nó qua thuộc tính không công khai SpecialPoovới lớp dẫn xuất AnimalWithSpecialPoo<TPoo>Excrementthuộc tính kiểu trả về dẫn xuất.

Nếu Catlà động vật duy nhất Poođặc biệt theo bất kỳ cách nào hoặc chúng tôi không muốn loại động vật Excrementtrở thành đặc điểm xác định chính của a Cat, thì lớp chung chung trung gian có thể bị bỏ qua trong hệ thống phân cấp, để dẫn Catxuất trực tiếp từ AnimalWithSpecialisations, nhưng nếu có là một số loài động vật khác nhau có đặc điểm cơ bản là chúng Poođặc biệt theo một cách nào đó, việc tách "boilerplate" thành các lớp trung gian giúp giữ cho Catbản thân lớp khá sạch sẽ, mặc dù phải trả thêm một vài lệnh gọi hàm ảo.

Mã ví dụ cho thấy rằng hầu hết các hoạt động mong đợi đều hoạt động "như mong đợi".

public interface IExcretePoo<out TPoo>
  where TPoo : Poo
{
  TPoo Excrement { get; }
}

public class Poo
{ }

public class RadioactivePoo : Poo
{ }

public class AnimalBase : IExcretePoo<Poo>
{
  public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
  // No override, just return normal poo like normal animal
}

public abstract class AnimalWithSpecialisations : AnimalBase
{
  // this class connects AnimalBase to AnimalWithSpecialPoo<TPoo>
  public sealed override Poo Excrement { get { return SpecialPoo; } }

  // if not overridden, our "special" poo turns out just to be normal animal poo...
  protected virtual Poo SpecialPoo { get { return base.Excrement; } }
}

public abstract class AnimalWithSpecialPoo<TPoo> : AnimalWithSpecialisations, IExcretePoo<TPoo>
  where TPoo : Poo
{
  sealed protected override Poo SpecialPoo { get { return Excrement; } }
  public new abstract TPoo Excrement { get; }
}

public class Cat : AnimalWithSpecialPoo<RadioactivePoo>
{
  public override RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

class Program
{
  static void Main(string[] args)
  {
    Dog dog = new Dog();
    Poo dogPoo = dog.Excrement;

    Cat cat = new Cat();
    RadioactivePoo catPoo = cat.Excrement;

    AnimalBase animal = cat;

    Poo animalPoo = catPoo;
    animalPoo = animal.Excrement;

    AnimalWithSpecialPoo<RadioactivePoo> radioactivePooingAnimal = cat;
    RadioactivePoo radioactivePoo = radioactivePooingAnimal.Excrement;

    IExcretePoo<Poo> pooExcreter = cat; // through this interface we don't know the Poo was radioactive.
    IExcretePoo<RadioactivePoo> radioactivePooExcreter = cat; // through this interface we do.

    // we can replace these with the dog equivalents:
    animal = dog;
    animalPoo = dogPoo;
    pooExcreter = dog;

    // but we can't do:
    // radioactivePooExcreter = dog;
    // radioactivePooingAnimal = dog;
    // radioactivePoo = dogPoo;
  }

0

C # 9 cung cấp cho chúng ta các kiểu trả về ghi đè hiệp phương sai. Về cơ bản: những gì bạn muốn chỉ hoạt động .


-1

Chà, thực sự có thể trả về một Kiểu cụ thể khác với Kiểu trả về kế thừa (ngay cả đối với các phương thức tĩnh), nhờ dynamic:

public abstract class DynamicBaseClass
{
    public static dynamic Get (int id) { throw new NotImplementedException(); }
}

public abstract class BaseClass : DynamicBaseClass
{
    public static new BaseClass Get (int id) { return new BaseClass(id); }
}

public abstract class DefinitiveClass : BaseClass
{
    public static new DefinitiveClass Get (int id) { return new DefinitiveClass(id);
}

public class Test
{
    public static void Main()
    {
        var testBase = BaseClass.Get(5);
        // No cast required, IntelliSense will even tell you
        // that var is of type DefinitiveClass
        var testDefinitive = DefinitiveClass.Get(10);
    }
}

Tôi đã triển khai điều này trong một trình bao bọc API mà tôi đã viết cho công ty của mình. Nếu bạn dự định phát triển một API, điều này có khả năng cải thiện khả năng sử dụng và trải nghiệm của nhà phát triển trong một số trường hợp sử dụng. Tuy nhiên, việc sử dụng dynamiccó ảnh hưởng đến hiệu suất, vì vậy hãy cố gắng tránh nó.

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.