我想避免使用将基类类型转换为派生类类型的方法,如果我想访问通用功能,但是如果我想要专用功能,那么我不能成功地做到这一点
我已经编写了代码来演示我已经尝试过的内容。
public abstract class Animal : IAnimal
{
public void Move()
{
}
}
public interface IAnimal
{
void Move();
}
public interface IDog:IAnimal
{
void bark();
}
public class Dog : IDog
{
public void Move()
{
}
public void bark()
{
}
}
static void Main(string[] args)
{
Animal animal = null;
IDog dog = animal as IDog;
dog.bark(); // can access specialized method
IAnimal puppy = new Dog();
puppy.Move(); // can only access generic functions
}
如何重新设计类以访问“树皮”方法而无需强制转换?
答案 0 :(得分:5)
简短的回答:您不能,也不应该。
相反,您可以做的是在MakeNoise()
接口中实现IAnimal
方法,因为您通常希望动物发出声音。
但是,如果您坚持将Bark()
保留在IDog
上,则不会期望IDuck
能够访问它-它应该有一个Quack()
方法。向下投射到IAnimal
的对象都不可用,因为您如何猜测是Duck
还是Dog
?
我将发布一些更多的“现实生活”示例,说明为什么在编程中可能需要继承,因为您提供的示例有点像“书籍示例”,因此它也是晦涩而模糊的。
using System.Collections.Generic;
namespace ConsoleApp1
{
public static class DocumentHandling
{
public static List<IAccountable> Documents;
public static dynamic InternalService { get; set; }
public static dynamic IRS { get; set; }
public static void HandleDocuments()
{
foreach (var document in Documents)
{
document.Account();
}
}
}
public interface IAccountable
{
void Account();
}
public abstract class Document
{
public int DatabaseId { get; set; }
public string Title { get; set; }
}
public abstract class DocumentWithPositions : Document
{
public int[] PositionsIds { get; set; }
}
public class Invoice : DocumentWithPositions, IAccountable
{
public void Account()
{
var positions = DocumentHandling.InternalService.PreparePositions(this.PositionsIds);
DocumentHandling.IRS.RegisterInvoice(positions);
}
}
public class Receipt : DocumentWithPositions, IAccountable
{
public void Account()
{
Invoice invoice = DocumentHandling.InternalService.ConvertToReceipt(this);
invoice.Account();
}
}
}
看看我如何将Invoice
和Receipt
这两个文档都填充到一个列表中(因为它们被向下转换为IAccountable
)?现在,即使它们的具体实现对会计过程的处理方式不同,我也可以一次全部对其进行会计处理。
答案 1 :(得分:0)
让我们先谈谈Liskov Substitution Principle,然后再谈OOP和继承。
首先,让我们谈谈Abstract Data Types。在她的论文中,她使用了来自类型的对象的概念。
摘要数据类型(ADT)是对类型的描述,其中包含所有操作和行为 。 ADT 的所有客户都应该知道使用它时的期望。
这是一个例子:
让我们将Stack
定义为ADT
操作: :push
,pop
,topElement
,size
,isEmpty
>
行为:
push
: 总是将元素添加到堆栈顶部! size
:返回堆栈中的元素数pop
:从堆栈顶部移除和元素。如果堆栈为空则报错topElement
:返回堆栈中的顶部元素。如果堆栈为空则报错isEmpty
:如果堆栈为空,则返回true,否则返回false 至此,我们就Stack
的操作及其行为方式进行了描述。在这里我们不是在谈论个案,也不是在讨论具体的实现。这就是 抽象数据类型 。
现在让我们建立类型层次结构。在C#中,接口和类都是类型。由于接口仅定义操作,因此它们是不同的,因此从某种意义上说它们是契约。它们定义了ADT的操作。通常人们会假设只有相互继承的类才能定义类型层次结构。确实可以将互相继承的类称为 Superclass 或 Baseclass 和 Subclass ,但是从 Types的角度来看对于接口和类,我们确实有 Supertype 和 Subtype ,因为它们都定义了类型。
注意:为简单起见,我将跳过方法实现中的错误检查
// interfaces are types. they define a contract so we can say that
// they define the operations of an ADT
public interface IStack<T> {
T Top();
int Size();
void Push(T element);
void Pop();
bool IsEmpty();
}
// the correct term here for C# whould be 'implements interface' but from
// point of view of ADTs and *Types* ListBasedStack is a *Subtype*
public class ListBasedStack<T> : IStack<T> {
private List<T> mElements;
public int Size() { return mElements.Count; }
public T Top() { mElements(mElements.Count - 1); }
public void Push(T element) { mElements.Add(element); }
public void Pop() { mElements.Remove(mElements.Count - 1); }
public bool IsEmpty() { return mElements.Count > 0; }
}
public class SetBasedStack<T> : IStack<T> {
private Set<T> mElements;
public int Size() { return mElements.Count; }
public T Top() { mElements.Last(); }
public void Push(T element) { mElements.Add(element); }
public void Pop() { mElements.RemoveLast(); }
public bool IsEmpty() { return mElements.Count > 0; }
}
请注意,我们有两个 Aem 相同的子类型。现在让我们考虑一个测试案例。
public class Tests {
public void TestListBasedStackPush() {
EnsureUniqueElementsArePushesToAStack(new ListBasedStack<int>());
}
public void TestSetBasedStackPush() {
EnsureUniqueElementsArePushesToAStack(new SetBasedStack<int>());
}
public void EnsureUniqueElementsArePushesToAStack(IStack<int> stack) {
stack.Push(1);
stack.Push(1);
Assert.IsTrue(stack.Size() == 2);
}
}
结果是:
TestListBasedStackPush
:通过TestSetBasedStackPush
:失败! SetBasedStack
违反了push
的规则: 总是将元素添加到堆栈的顶部! ,因为集合只能包含唯一的元素元素,第二个stack.Push(1)
不会将新元素添加到堆栈中。
这违反了LSP。
现在介绍示例,并输入诸如IAnimal
和Dog
之类的层次结构。当您处于 正确的摘要级别 时,一种类型的行为应与预期的一致。如果确实需要Dog
,请使用Dog
。如果您确实需要IAnimal
,请使用IAnimal
。
如果有Bark
,您如何访问IAnimal
? 您不要! 。您处于错误的抽象级别。如果确实需要Dog
,请使用Dog
。如果需要,请投射。
public class Veterenerian {
public void ClipDogNails(IAnimal animal) { } // NO!
public void ClipDogNails(Dog dog) { } // YES!
}
private Veterenerian mOnDutyVeterenerian;
private List<IAnimal> mAnimals;
public ClipAllDogsNails() {
// Yes
foreach(var dog in mAnimals.OffType<Dog>()) {
mOnDutyVeterenerian.ClipDogNails(dog);
}
// NO
foreach(var animal in mAnimals) {
mOnDutyVeterenerian.ClipDogNails(animal);
}
}
您需要投射吗?有时是的。如果最好不这样做呢?是的,大多数时候都是这样。
您如何解决上述问题?您可以将Dog Clip做成自己的指甲。您是否正在将方法ClipNails
添加到IAnimal
,并使仅带有指甲的动物实现此方法,而使其他动物子类保留此方法为空?没有!因为在IAnimal
和的抽象级别上没有意义,所以它也违反了LSP。另外,如果您执行此操作,可以致电animal.ClipNails()
,这样就可以了,但是如果您有时间表说狗应该在其他动物的星期五修剪指甲,那么星期一又会卡住,因为您可以让所有动物都修剪它们的指甲,不仅是狗。
有时,一个 Type 的对象将被另一个 Type 的对象使用。有些操作在类型中没有意义。这个例子说明了狗怎么不能剪指甲。应该由Veterenerial
完成。
但是,我们确实需要在IAnimal
抽象级别上进行工作。 Veterenerian Clinic
中的所有事物都是动物。但是有时需要对动物的特定类型(在这种情况下为operations
)执行某些Dog
,因此我们确实需要过滤<按其 类型 的strong> 动物 。
但这与上面的Stack
示例完全不同。
下面是一个示例,说明何时不应该使用强制类型转换,并且客户端代码不应包含具体实现的情况:
public abstract class Serializer {
public abstract byte[] Serialize(object o);
}
public class JSONSerializer : Serializer {
public override byte[] Serialize(object o) { ... }
}
public class BinarySerializer : Serializer {
public override byte[] Serialize(object o) { ... }
}
public void DoSomeSerialization(Serializer serializer, Event e) {
EventStore.Store(serializer.Serialize(e));
}
DoSomeSerialization
方法 不应该关心传递给它的串行器。您可以通过符合Serializer
规范的 任何 Serializer
,它应该可以工作。这就是具有多个实现的抽象的意义。 DoSomeSerialization
在Serializer
级别上工作。我们可以将Serializer
定义为ADT。从Serializer
派生的所有类都应遵守ADT的规范,并且系统运行正常。这里没有强制转换,因为问题不同,因此无需强制转换。