为什么要将对象转换为接口?

42

在Jesse Liberty的《Programming C#》(第142页)中,他提供了一个示例,在该示例中,他将对象转换为接口。

 interface IStorable
 {
    ...
 }

 public class Document : IStorable
 {
    ...
 }

 ...
 IStorable isDoc = (IStorable) doc;  
 ...

如果对象的类已经实现了该接口,那么这样做的目的是什么?

编辑1: 为了澄清,我对于强制类型转换的原因很感兴趣,而不是实现接口的原因。另外,这本书是他2001年的第一版(基于C#1),所以这个示例可能对于后续版本的C#来说不相关。

编辑2: 我添加了一些代码上下文。


我的第一个猜测是显式用户定义转换,但实际上根据(6.4.1)的规定,您不能将显式用户定义强制转换为接口。 - Tamas Czinege
我已经在最新版本中找到了相应的部分,但看起来有些不同。 - Jon Skeet
@Jon - 谢谢你查找这个。 - eft
你的意思是为什么要进行显式接口转换,还是说你想问“为什么要将类型向下转换为接口?” - Randolpho
1
我的意思是为什么要进行显式转换,它是否必要/有帮助/多余? - eft
12个回答

26
因为您希望仅使用接口提供的方法。如果使用类,您可能会意外调用不属于接口的方法。

1
你能不能不用 IStorable isDoc = doc; 这种方式,而是直接判断 doc 是否为 IStorable 接口的实现类呢?这样的话强制类型转换就显得有些多余了,除非我把 Java 和 C# 搞混了。(很遗憾,我已经有一段时间没写 C# 了。) - Devin Jeanpierre
2
@Devin:是的,只要doc被声明为实现了该接口的类型,转换就是多余的。 - Jon Skeet
参考本身不会限制您从接口外调用任何方法吗? - Bill the Lizard

20

只有一种情况需要进行类型转换:当“doc”是实现了“IStorable”的对象的基本类型时。让我解释一下:

public class DocBase
{
  public virtual void DoSomething()
  {

  }
}

public class Document : DocBase, IStorable
{
  public override void DoSomething()
  {
    // Some implementation
    base.DoSomething();
  }

  #region IStorable Members

  public void Store()
  {
    // Implement this one aswell..
    throw new NotImplementedException();
  }

  #endregion
}

public class Program
{
  static void Main()
  {
    DocBase doc = new Document();
    // Now you will need a cast to reach IStorable members
    IStorable storable = (IStorable)doc;
  }
}

public interface IStorable
{
  void Store();
}

2
谢谢,但这让我有点困惑。为什么在创建文档对象时要使用基本类型DocBase? - eft
可能的一个原因是你在使用工厂构造对象时返回了对象的基本类型。 - Chetan S
2
可能有任意数量的原因。我看到的大多数情况是通过实现由第三方提供并未指定接口的基类。在这种情况下,“DocBase”类来自不同的库,并在您自己的代码中使用接口实现。 - Jeroen Landheer
5
为什么在将编译时已知类型实现IStorable时,使用转换运算符向IStorable变量赋值,而不是使用EDIT2的方法,这个问题没有得到解释。 - binki
1
// 现在你需要一个转换才能访问IStorable成员 -- 这是上面代码中的写法,但是:使用以下方式仍然可以访问IStorable成员吗: doc.Store() ?? - BenKoshy
1
@BenKoshy:不带强制转换是无法访问doc.Store()的,因为doc被声明为DocBase,而该类型并未实现IStorable接口。 - Basil

15
如果对象明确实现了接口(public void IStorable.StoreThis(...)),那么强制转换是实际访问接口成员最简单的方法。

有没有其他方法可以访问显式接口成员? - Svish
4
这是唯一一种达到显式实现接口成员的方法。显式实现的原因是你想让类更加简洁,因为实现可能很少使用。 - Greg Gum

14

我不确定书中给出的示例是在什么情况下使用的。但是,您通常可以将对象强制转换为接口以实现多重继承。我已经给出了以下示例。

public interface IFoo
{
     void Display();
}
public interface IBar
{
     void Display();
}

public class MyClass : IFoo, IBar
{
    void IBar.Display()
    {
        Console.WriteLine("IBar implementation");
    }
    void IFoo.Display()
    {
        Console.WriteLine("IFoo implementation");
    }
}

public static void Main()
{
    MyClass c = new MyClass();
    IBar b = c as IBar;
    IFoo f = c as IFoo;
    b.Display();
    f.Display();
    Console.ReadLine();
}

这将显示

IBar 实现
IFoo 实现


10

没有更多的上下文很难确定。如果变量doc被声明为实现该接口的类型,则强制转换是冗余的。

你正在阅读哪个版本的书?如果是《Programming C# 3.0》,我晚上回家后会看一下。

编辑:就目前为止,我们已经看到了三个潜在的问题:

  • 为什么在问题中显示的语句中进行强制转换?(答案:如果doc是适当的编译时类型,则不必这样做)
  • 为什么明确地强制转换为已实现的接口或基类是合适的? (答案:如另一个答案所示,明确接口实现,以及在将强制转换值作为参数传递时选择较少特定的重载。)
  • 为什么使用接口? (答案:使用接口类型工作意味着您对以后的具体类型的更改更不容易受到影响。)

"(IStorable)"强制转换还通过明确声明“doc”不是一个“IStorable”对象,而是可以被转换为“IStorable”的东西,从而帮助可读性。虽然有些多余,但它为以后可能阅读代码的任何人添加了这一信息。 - DevinB
3
我不认为那是一个有用的信息,事实上,对我来说这意味着doc变量是一种需要在执行时检查的类型(听起来在这里并非如此),这是误导性的。 - Jon Skeet
(答案:如果文档是适当的编译时类型,则您不必这样做。)只有一个字母,但它可以产生天壤之别。 - BenKoshy

3

doc 对象可能是一种实现了 IStorable 成员的类型,它们没有被添加到类的主要接口中(即它们只能通过接口调用)。

实际上,“强制类型转换”(使用 (T) 语法)没有任何意义,因为 C# 自动处理向上转型(转换为父类型),与 F# 不同。


2

这里有很多好的答案,但我不认为它们真正回答了为什么你实际上想要使用可能最严格的接口。

原因并不涉及您的初始编码,而是涉及下次访问或重构代码时--或者是其他人进行代码编辑时--会发生什么。

假设您想要一个按钮并将其放置在屏幕上。您通过传递或从另一个函数中获取该按钮,如下所示:

Button x=otherObject.getVisibleThingy();
frame.add(x);

您知道VisibleThingy是一个按钮,它返回一个按钮,所以一切都很好(不需要强制转换)。

现在,假设您将VisibleThingy重构为返回一个切换按钮。您现在必须重构您的方法,因为您对实现了解得太多了。

由于您只需要Component中的方法(button和Toggle的父级,这可以是接口 - 对于我们的目的来说几乎相同),如果您最初编写第一行代码如下:

Component x=(Component)otherObject.getVisibleThingy();

你不需要重构任何东西,它只是起作用了。

这是一个非常简单的情况,但它也可以更加复杂。

所以我想总结一下,接口是查看对象的特定方法--就像通过过滤器查看它...你只能看到一些部分。如果你能够限制你的视角,对象可以在你特定的视角下“变形”,而不会影响你当前的世界--这是抽象的一个非常强大的技巧。


2
你需要转换为接口的最好原因是,如果你正在编写针对对象的代码,而你不知道它们具体的类型,也不想知道。如果你知道可能会遇到实现特定接口的对象,那么你可以从对象中获取值,而不必知道该对象的具体类。此外,如果你知道一个对象实现了给定的接口,那么该接口可能定义了你可以执行的方法,以对该对象执行某些操作。
以下是一个简单的例子:
public interface IText
{
   string Text { get; }
}

public interface ISuperDooper
{
   string WhyAmISuperDooper { get; }
}

public class Control
{
   public int ID { get; set; }
}

public class TextControl : Control, IText
{
   public string Text { get; set; }
}

public class AnotherTextControl : Control, IText
{
   public string Text { get; set; }
}

public class SuperDooperControl : Control, ISuperDooper
{
   public string WhyAmISuperDooper { get; set; }
}

public class TestProgram
{
   static void Main(string[] args)
   {
      List<Control> controls = new List<Control>
               {
                   new TextControl
                       {
                           ID = 1, 
                           Text = "I'm a text control"
                       },
                   new AnotherTextControl
                       {
                           ID = 2, 
                           Text = "I'm another text control"
                       },
                   new SuperDooperControl
                       {
                           ID = 3, 
                           WhyAmISuperDooper = "Just Because"
                       }
               };

       DoSomething(controls);
   }

   static void DoSomething(List<Control> controls)
   {
      foreach(Control control in controls)
      {
         // write out the ID of the control
         Console.WriteLine("ID: {0}", control.ID);

         // if this control is a Text control, get the text value from it.
         if (control is IText)
            Console.WriteLine("Text: {0}", ((IText)control).Text);

         // if this control is a SuperDooperControl control, get why
         if (control is ISuperDooper)
            Console.WriteLine("Text: {0}", 
                ((ISuperDooper)control).WhyAmISuperDooper);
      }
   }
}

运行这个小程序会输出以下结果:

ID:1

文本:我是一个文本控件

ID:2

文本:我是另一个文本控件

ID:3

文本:只是因为

请注意,我不需要在DoSomething方法中编写任何代码,也不需要知道所有工作对象都是具体对象类型。我唯一知道的是,我正在处理至少是Control类实例的对象。然后,我可以使用接口查找它们可能具有的其他内容。
对于对象使用接口的原因有很多,但它为您提供了一种松散的访问对象的方式,而无需确切知道对象是什么。
想象一下世界上所有的信用卡,每个公司都有自己的信用卡,接口却是相同的,因此每个读卡器都可以刷过遵循标准的信用卡。与使用接口类似。

1
如已指出,强制转换是多余的且不必要的。然而,这是一种更明确的编码形式,对于初学者在帮助他们理解方面非常有用。
在入门教材中,最好显式地执行操作,而不是让编译器隐式处理,这会对初学者更加困惑。
“doc”不属于“IStorable”类型,因此初学者看到它被分配给isDoc会感到困惑。通过显式转换,作者(书籍和代码的作者)表明一个文档可以被转换为IStorable对象,但它并不是同一个IStorable对象。

0
重点是,对象(你从哪里得到的?)可能没有实现接口,在这种情况下会抛出异常,可以捕获并处理。当然,您可以使用“is”运算符进行检查,并使用“as”运算符进行转换,而不是使用C样式转换。

有人可以解释一下为什么你要投反对票吗?谢谢。 - royatl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接