作者将接口引用强制转换为任何实现是什么意思?


17

我目前在试图掌握C#的过程,所以我读通过C#自适应编码加里·麦克莱恩大厅

他撰写有关模式和反模式的文章。在实现与接口部分中,他写道:

不熟悉接口编程概念的开发人员通常很难放开接口背后的内容。

在编译时,接口的任何客户端都不应该知道接口正在使用哪种接口实现。这些知识可能导致错误的假设,从而使客户端耦合到接口的特定实现。

想象一下一个常见的示例,其中一个类需要在持久性存储中保存一条记录。为此,它正确地委托给一个接口,该接口隐藏了所使用的持久性存储机制的详细信息。但是,对运行时使用接口的哪个实现进行任何假设都是不正确的。例如,将接口引用强制转换为任何实现都是一个坏主意。

这可能是语言障碍,或者是我缺乏经验,但是我不太明白这意味着什么。这是我的理解:

我有一个免费的有趣的项目来练习C#。我在那里上课:

public class SomeClass...

此类在很多地方都使用过。在学习C#时,我读到最好使用接口抽象,因此我做了以下工作

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

因此,我进入了所有一些类引用,并用ISomeClass替换了它们。

除了在构造中,我写过:

ISomeClass myClass = new SomeClass();

我是否正确理解这是错误的?如果是,为什么这样做,我应该怎么做呢?


25
在示例中,您没有将接口类型的对象转换为实现类型的对象。您正在为接口变量分配实现类型,这是完全正确且正确的。
卡雷斯(Caleth)'17

1
“我写的构造函数中是什么意思ISomeClass myClass = new SomeClass();?”如果您真的是那个意思,那是构造函数中的递归,可能不是您想要的。希望您指的是“构造”,即分配,但不是构造函数本身,对吧。 ?
Erik Eidt's

@Erik:是的。施工中。你是对的。将纠正问题。谢谢
Marshall

有趣的事实:在这方面,F#比C#更好的故事-它消除了隐式接口的实现,因此,每当要调用接口方法时,都需要转换为接口类型。这使您很清楚何时以及如何在代码中使用接口,并使对接口的编程更加扎根于该语言中。
scrwtp

3
这有点题外话,但是我认为作者误诊了这个概念的新手所遇到的问题。在我看来,问题在于刚接触该概念的人不知道如何建立良好的界面。制作实际上不提供任何通用性的太具体的接口非常容易(这很可能发生在ISomeClass),但是制作太通用的接口也很容易,因此无法针对此编写有用的代码,这是唯一的选择是重新考虑界面并重写代码或向下转换。
德里克·埃尔金斯

Answers:


37

只有当您打算编写该接口的其他实现,或者将来存在这样做的可能性很大时,才应该考虑将类抽象到一个接口中。

因此,也许SomeClassISomeClass是一个坏榜样,因为它会像有一个OracleObjectSerializer类和IOracleObjectSerializer接口。

一个更准确的示例将是OracleObjectSerializerIObjectSerializer。程序中您唯一关心使用哪种实现的地方是创建实例时。有时,这可以通过使用工厂模式进一步分离。

程序中的其他任何地方都不应使用IObjectSerializer它的工作方式。现在让我们假设您SQLServerObjectSerializer除了之外还有一个实现OracleObjectSerializer。现在,假设您需要设置一些特殊的属性,并且该方法仅在OracleObjectSerializer中存在,而在SQLServerObjectSerializer中不存在。

有两种解决方法:不正确的方法和Liskov替代原理方法。

错误的方式

错误的方式以及书中所引用的实例都是将实例化IObjectSerializer并将其强制转换为OracleObjectSerializer,然后调用setProperty仅在上可用的方法OracleObjectSerializer。这很不好,因为即使您可能知道实例是,也要OracleObjectSerializer在程序中引入另一个要点,即您要知道实例是什么。当该实现发生更改时,如果您有多个实现(最好的情况),则大概迟早会发现所有这些地方并做出正确的调整。最坏的情况是,您将IObjectSerializer实例转换为,OracleObjectSerializer并且在生产中收到运行时故障。

里斯科夫替代原理法

Liskov说,如果正确完成,您永远都不需要像setProperty实现类中那样的方法OracleObjectSerializer。如果将类抽象OracleObjectSerializerIObjectSerializer,则应包含使用该类所需的所有方法,如果不能使用,则抽象有问题(例如尝试使Dog类作为IPerson实现工作)。

正确的方法是提供一种setProperty方法IObjectSerializerSQLServerObjectSerializer理想地,类似的方法将通过该setProperty方法工作。更好的是,您通过Enum每个实现将枚举转换为其自身数据库术语的等效项的方式来标准化属性名称。

简而言之,使用an ISomeClass只是它的一半。您永远不需要将其强制转换到负责其创建的方法之外。这样做几乎肯定是一个严重的设计错误。


1
在我看来,如果你要投IObjectSerializerOracleObjectSerializer,因为你“知道”,这是它是什么,那么你就应该对自己诚实的(更重要的是,与其他人谁可能保持这种代码,其中可能包括你的未来自我),并OracleObjectSerializer从创建到使用一直使用。这使其非常公开,并且很清楚,您正在引入对特定实现的依赖关系-并且这样做所涉及的工作和丑陋本身就很容易暗示某些问题是错误的。
KRyan

(而且,如果由于某种原因,你真的不得不依赖于特定的实现,它变得更加清晰,这是你在做什么,你是故意和目的这样做。这“应该”不会发生,当然,和99%的时间似乎确实发生了,这实际上不是您应该解决的问题,但没有100%确定或应该采取的方式。)
KRyan

@KRyan绝对。仅在需要时才应使用抽象。在不必要时使用抽象只会使代码难于理解。
尼尔(Neil)

29

可接受的答案是正确的,而且非常有用,但是我想简要地简要介绍一下您询问的代码行:

ISomeClass myClass = new SomeClass();

从广义上讲,这并不可怕。应该尽可能避免这样做:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

在向您的代码提供外部接口,但在内部将其强制转换为特定实现的地方,“因为我知道它只会是该实现”。即使最终确实是对的,通过使用接口并将其强制转换为实现,您还是自愿放弃了实类型安全性,以便您可以假装使用抽象。如果其他人稍后要处理代码并看到接受接口参数的方法,那么他们将假定该接口的任何实现都是有效的传递方法。它甚至可能会使您有点失望在您忘记了一种特定的方法取决于所需的参数之后,请注意以下几点。如果您觉得有必要从接口转换为特定的实现,则可以选择该接口,该实现,或引用它们的代码设计不正确,应该更改。例如,如果仅当传入的参数是特定类时该方法才起作用,则参数应仅接受该类。

现在,回顾您的构造函数调用

ISomeClass myClass = new SomeClass();

投放问题并没有真正应用。这些似乎都没有暴露在外部,因此没有任何特殊的风险。从本质上讲,这行代码本身是一个实现细节,接口被设计为从抽象开始,因此外部观察者将看到接口以相同的方式工作,而不管它们做什么。但是,这也不会因接口的存在而获得任何收益。您myClass的类型为ISomeClass,但没有任何原因,因为它始终分配有特定的实现,SomeClass。有一些较小的潜在优势,例如能够通过仅更改构造函数调用来交换代码中的实现,或者稍后将该变量重新分配给其他实现,但是除非有其他地方要求将变量键入接口而不是接口这种模式的实现使您的代码看起来像接口仅由死记硬背使用,而不是出于对接口好处的实际了解。


1
Apache Math使用Cartesian3D Source的286行来完成此操作,这确实很烦人。
J_F_B_M

1
这是解决原始问题的实际正确答案。
本杰明·格伦鲍姆

2

我认为用一个不好的例子显示代码会更容易:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

问题在于,当您最初编写代码时,该接口可能只有一个实现,因此强制转换仍然可以工作,只是在将来,您可以实现另一个类,然后(如我的示例所示),尝试访问您正在使用的对象中不存在的数据。


先生,你好 有人告诉过你你有多帅吗?
尼尔

2

为了清楚起见,让我们定义转换。

强制转换是将某种东西从一种类型转换为另一种类型。一个常见的示例是将浮点数转换为整数类型。转换时可以指定特定的转换,但是默认转换是简单地重新解释这些位。

这是此Microsoft文档页面上的强制转换示例。

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

可以做同样的事情,然后将实现接口的内容转换为该接口的特定实现,但是您不应该这样做,因为如果使用与预期不同的实现,则会导致错误或意外行为。


1
“铸造正在将某种东西从一种类型转换为另一种类型。” -不会。转换明确地将某种东西从一种类型转换为另一种类型。(特别是,“ cast”是用于指定该转换的语法的名称。)隐式转换不是强制转换。“在转换时可以指定一个特定的转换,但是默认是简单地重新解释这些位。” -当然不是。有很多转换,包括隐式转换和显式转换,都涉及对位模式的重大更改。
hvd

@hvd我现在对铸造的显式性进行了更正。当我说默认值是简单地重新解释位时,我试图表达的是,如果您要创建自己的类型,那么在自动定义强制类型转换的情况下,当将其强制转换为另一种类型时,这些位将被重新解释。在上面的Animal/ Giraffe示例中,如果要这样做Animal a = (Animal)g;,将重新解释这些位((任何长颈鹿特定的数据都将被解释为“不是该对象的一部分”))。
Ryan1729

尽管hvd说了什么,但人们还是经常使用术语“ cast”来表示隐式转换。参见例如https://www.google.com/search?q="implicit+cast"&tbm=bks。从技术上讲,我认为保留“ cast”一词用于显式转换是更正确的,只要您在其他人使用不同的用法时不会感到困惑。
ruakh

0

我的5美分:

所有这些示例都可以,但是它们不是真实世界的示例,也没有显示真实世界的意图。

我不知道C#,所以我将给出抽象的示例(Java和C ++之间的混合)。希望没事。

假设您有接口iList

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

现在假设有很多实现:

  • DynamicArrayList-使用平面数组,可在最后快速插入和删除。
  • LinkedList-使用双链表,可快速插入到开头和结尾。
  • AVLTreeList-使用AVL树,可以快速完成所有操作,但占用大量内存
  • SkipList-使用SkipList可以快速完成所有任务,比AVL Tree慢,但使用较少的内存。
  • HashList-使用HashTable

可以想到许多不同的实现。

现在假设我们有以下代码:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

它清楚地表明了我们要使用的意图iList。当然,我们不再能够执行DynamicArrayList特定操作,但是我们需要一个iList

考虑以下代码:

iList list = factory.getList();

现在我们什至不知道实现是什么。当您从磁盘加载某些文件并且不需要其文件类型(gif,jpeg,png,bmp ...)时,最后一个示例通常用于图像处理,但是您要做的只是做一些图像处理(翻转,缩放,最后另存为png)。


0

您有一个接口ISomeClass,还有一个对象myObject,您对代码的了解不多,只是声明要实现ISomeClass。

您有一个SomeClass类,您知道该类实现了ISomeClass接口。您知道这是因为它是声明为实现ISomeClass的,或者您是自己实现以实现ISomeClass的。

将myClass强制转换为SomeClass有什么问题?有两件事是错误的。第一,您真的不知道myClass是可以转换为SomeClass(SomeClass的实例或SomeClass的子类)的东西,因此强制转换可能会出错。第二,您不必这样做。您应该使用声明为iSomeClass的myClass并使用ISomeClass方法。

获得SomeClass对象的关键是调用接口方法时。在某个时候,您可以调用myClass.myMethod(),该方法在接口中声明,但是在SomeClass中实现,当然还有可能在实现ISomeClass的许多其他类中实现。如果调用以您的SomeClass.myMethod代码结束,则您知道self是SomeClass的实例,在那一点上是完全可以的,并且实际上可以将其用作SomeClass对象。当然,如果它实际上是OtherClass的实例而不是SomeClass的实例,那么您将不会得到SomeClass代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.