向下转换的正确用法是什么?


67

向下转换意味着从基类(或接口)转换为子类或叶类。

如果您从转换System.Object为其他类型,则可能会导致沮丧。

向下转换不受欢迎,可能有代码味道:例如,面向对象的理论更喜欢定义和调用虚拟或抽象方法,而不是向下转换。

  • 什么是向下转换的好用例(如果有)?也就是说,在什么情况下编写向下转换的代码是合适的?
  • 如果您的答案为“无”,那么为什么该语言支持向下转换?

5
您所描述的通常称为“向下转换”。有了这些知识,您是否可以先做一些自己的研究,并解释为什么发现的现有原因不够充分?TL; DR:向下转换会破坏静态类型,但有时类型系统的限制过于严格。因此,对一种语言进行安全的向下转换是明智的。
amon

你是说垂头丧气吗?向上转换将通过类层次结构进行,即从子类到超类,而不是向下转换,从超类到子类...
Andrei Socaciu 18'2 /

抱歉:您是对的。我编辑了问题,将“ upcast”重命名为“ downcast”。
ChrisW '18

@amon en.wikipedia.org/wiki/Downcasting以“转换为字符串”为例(.Net支持使用virtual ToString);它的另一个示例是“ Java容器”,因为Java不支持泛型(C#支持)。stackoverflow.com/questions/1524197/downcast-and-upcast说向下转换什么,但没有当它的例子适当
ChrisW '18

4
@ChrisW那before Java had support for generics不是because Java doesn't support generics。而amon几乎为您提供了答案-当您使用的类型系统过于严格,并且您无权对其进行更改时。
Ordous

Answers:


12

这是向下转换的一些正确用法。

我在此强烈地不同意其他人的意见,他们说使用向下转换绝对是一种代码味道,因为我相信没有其他合理的方法可以解决相应的问题。

Equals

class A
{
    // Nothing here, but if you're a C++ programmer who dislikes object.Equals(object),
    // pretend it's not there and we have abstract bool A.Equals(A) instead.
}
class B : A
{
    private int x;
    public override bool Equals(object other)
    {
        var casted = other as B;  // cautious downcast (dynamic_cast in C++)
        return casted != null && this.x == casted.x;
    }
}

Clone

class A : ICloneable
{
    // Again, if you dislike ICloneable, that's not the point.
    // Just ignore that and pretend this method returns type A instead of object.
    public virtual object Clone()
    {
        return this.MemberwiseClone();  // some sane default behavior, whatever
    }
}
class B : A
{
    private int[] x;
    public override object Clone()
    {
        var copy = (B)base.Clone();  // known downcast (static_cast in C++)
        copy.x = (int[])this.x.Clone();  // oh hey, another downcast!!
        return copy;
    }
}

Stream.EndRead/Write

class MyStream : Stream
{
    private class AsyncResult : IAsyncResult
    {
        // ...
    }
    public override int EndRead(IAsyncResult other)
    {
        return Blah((AsyncResult)other);  // another downcast (likely ~static_cast)
    }
}

如果您要说这些是代码异味,则需要为它们提供更好的解决方案(即使由于不便而避免使用它们)。我认为没有更好的解决方案。


9
“代码气味”并不表示“此设计绝对不好 ”,而是表示“此设计可疑 ”。语言天生具有可疑的特征,这是语言作者的实用主义问题
Caleth,

5
@Caleth:我的意思是,这些设计一直都在出现,对于我所展示的设计,本来就没有任何本质上的可疑之处,因此强制转换不是代码的味道。
Mehrdad '18

4
@Caleth我不同意。对我来说,代码异味意味着代码不好,或者至少可以改进。
康拉德·鲁道夫'18

6
@Mehrdad您的意见似乎是代码气味一定是应用程序程序员的错。为什么?并非每个代码气味都必须是一个实际问题或有解决方案。根据Martin Fowler的说法,代码气味只是“通常对应于系统中更深层问题的表面指示” object.Equals,非常符合该描述(尝试在允许子类正确实现它的超类中正确提供均等合同)-绝对是不平凡的。作为应用程序程序员,我们无法解决它(在某些情况下我们可以避免)是另一回事。
Voo

5
@Mehrdad好,让我们看看一位原始语言设计师对Equals Object.Equals is horrid. Equality computation in C# is completely messed up的评价。(Eric对他的回答的评论)。有趣的是,即使该语言的首席设计师之一告诉您您错了,您也不想重新考虑自己的意见。
Voo

135

垂头丧气是不受欢迎的,也许是代码气味

我不同意。垂头丧气非常流行 ; 大量的现实世界节目包含一个或多个向下转换。而且这可能不是代码的味道。这是绝对一个代码味道。 这就是为什么要求向下转换操作必须在程序文本中显示的原因。这样一来,您可以更轻松地注意到气味,并花费代码对其进行关注。

在什么情况下编写向下转换的代码合适?

在任何情况下:

  • 您对表达式的运行时类型的事实有100%的正确认识,该事实比表达式的编译时类型更具体,并且
  • 您需要利用这一事实,以使用编译时类型上不可用的对象功能,并且
  • 与重构程序以消除前两点中的任何一个相比,更好地利用时间和精力来编写演员表。

如果您可以廉价地重构程序,以便可以由编译器推断运行时类型,或者可以重构程序以使您不需要更多派生类型的功能,则可以这样做。对于那些难以重构程序且昂贵的情况,在语言中添加了向下转换。

语言为何支持向下转换?

C#是由谁有工作做,务实的程序员发明谁有工作做务实的程序员。C#设计人员不是面向对象的纯粹主义者。而且C#类型系统并不完美;通过设计,它低估了可以对变量的运行时类型施加的限制。

同样,向下转换在C#中非常安全。我们有力保证将在运行时对向下转换进行验证,如果无法对其进行验证,则程序将执行正确的操作并崩溃。这太好了;这意味着,如果您对类型语义的100%正确理解是99.99%正确,那么您的程序将在99.99%的时间内工作,并在其余时间崩溃,而不是行为异常并在0.01%的时间内破坏用户数据。


练习:至少有一种方法可以在使用显式强制转换运算符的情况下用C#产生下调。您能想到任何这种情况吗?由于这些也是潜在的代码异味,因此您认为在设计功能时会导致向下崩溃而没有在代码中强制转换的设计因素是什么?


101
记住那些高中时代墙上的海报:“流行并不总是对的,正确的并不总是对的?” 您以为他们试图阻止您吸毒或在高中怀孕,但实际上他们是在警告技术债务的危险。
corsiKa

14
当然,@ EricLippert,foreach语句。这是在IEnumerable通用之前完成的,对吧?
Arturo TorresSánchez18年

18
这种解释使我很高兴。作为一名老师,我经常被问到为什么用这种方式设计C#,而我的答案通常基于您和您的同事在这里和您的博客上分享的动机。通常很难回答后续问题“但是,为什么?!”。在这里,我找到了一个完美的后续答案:“ C#是有工作要做的实用程序员为有工作要做的实用程序员发明的。”。:-)谢谢@EricLippert!
扎诺'18

12
旁白:显然,在我的评论上面我的意思是说我们有一个沮丧的从A到B.我导航类层次结构的时候真的不喜欢使用“向上”和“向下”和“父”与“子”的,我让他们在我的写作中一直都是错误的。类型之间的关系是超集/子集关系,所以我不知道为什么要有向上或向下的方向。而且甚至不让我开始“父母”。长颈鹿的父母不是哺乳动物,长颈鹿的父母是长颈鹿夫妇。
埃里克·利珀特

9
Object.Equals恐怖。C#中的平等计算完全被搞砸了,在评论中也太混乱了。代表平等的方法有很多。使它们不一致很容易;这很容易,实际上需要违反相等运算符所需的属性(对称性,自反性和可传递性)。今天,如果我是从头开始设计一个类型系统,那么根本不会EqualsGetHashcodeToStringObject
埃里克·利珀特

33

事件处理程序通常具有签名MethodName(object sender, EventArgs e)。在某些情况下,可以处理事件而无需考虑是什么类型sender,甚至根本不使用sender任何类型,但是在其他情况下,sender必须将其转换为更特定的类型来处理事件。

例如,您可能有两个,TextBox而您想使用一个委托来处理每个事件。然后,您需要强制sender转换为,TextBox以便您可以访问必要的属性来处理事件:

private void TextBox_TextChanged(object sender, EventArgs e)
{
   TextBox box = (TextBox)sender;
   box.BackColor = string.IsNullOrEmpty(box.Text) ? Color.Red : Color.White;
}

是的,在此示例中,您可以创建自己的子类,TextBox该子类在内部完成。但是,并非总是可行或不可能的。


2
谢谢,这是一个很好的例子。该TextChanged事件是-的成员Control-我想知道为什么sender不是类型Control而不是类型object?我也想知道框架是否(从理论上来说)是否已经为每个子类重新定义了事件(以便例如TextBox可以使用TextBox发送者的类型来获得事件的新版本)……是否(或可能不需要)向下转换(到TextBox)内TextBox执行(执行新的事件类型),但将避免需要在应用程序代码的事件处理程序垂头丧气。
ChrisW '18

3
这被认为是可以接受的,因为如果每个事件都有其自己的方法签名,则很难概括事件处理。尽管我认为这是一个公认的限制,而不是被认为是最佳做法,因为替代方法实际上更麻烦。如果有可能以a TextBox sender而不是a开头object sender而不会使代码过于复杂,那么我相信它会完成。
尼尔,

6
@Neil事件系统专门用于允许将任意数据块转发到任意对象。如果没有运行时检查类型正确性,这几乎是不可能的。
Joker_vD

@Joker_vD理想情况下,API当然可以使用通用的逆差支持强类型的回调签名。.NET(绝大多数)不执行此操作的事实仅仅是由于泛型和协方差稍后引入的事实。—换句话说,由于语言/ API的不足,需要在此处(以及通常在其他地方)进行向下转换,而不是因为从根本上来说这不是一个好主意。
康拉德·鲁道夫

@KonradRudolph当然可以,但是如何将任意类型的事件存储在事件队列中?您是否摆脱了排队并直接进行派遣?
Joker_vD

17

某些东西给您一个超类型时,您需要向下转换,并且您需要根据子类型来不同地处理它。

decimal value;
Donation d = user.getDonation();
if (d is CashDonation) {
     value = ((CashDonation)d).getValue();
}
if (d is ItemDonation) {
     value = itemValueEstimator.estimateValueOf(((ItemDonation)d).getItem());
}
System.out.println("Thank you for your generous donation of $" + value);

不,这闻起来不好。在理想情况下,Donation将有一个抽象方法,getValue并且每个实现子类型都将具有一个适当的实现。

但是,如果这些类是来自某个库的,您无法更改或不想更改该怎么办?然后没有其他选择。


这看起来是使用访问者模式的好时机。或dynamic达到相同结果的关键字。
user2023861

3
@ user2023861不,我认为访客模式取决于Donation子类的合作-即Donation需要声明一个抽象void accept(IDonationVisitor visitor),其子类通过调用的特定(例如,重载)方法来实现该抽象IDonationVisitor
ChrisW '18

@ChrisW,您是正确的。没有合作,您仍然可以使用dynamic关键字来实现。
user2023861 '18

在这种特殊情况下,您可以将DonationValue方法添加到Donation中。
immibis

@ user2023861:dynamic仍然垂头丧气,只是速度较慢。
Mehrdad

12

由于我无法发表评论,因此请添加到埃里克·利珀特的答案中 ...

您通常可以通过重构API以使用泛型来避免低估。但是直到2.0版才向语言中添加泛型。因此,即使您自己的代码不需要支持古代语言版本,您也可能会发现自己使用未参数化的旧类。例如,可能定义了一些类来承载type的有效负载Object,您必须将其强制转换为您真正知道的类型。


确实,有人可能会说Java或C#中的泛型实质上只是在再次使用元素之前存储超类对象并向下转换到正确的(由编译器检查的)子类的安全包装。(与C ++模板不同,后者重写代码以始终使用子类本身,从而避免了向下转换-如果经常以牺牲编译时间和可执行文件的大小为代价,则可以提高内存性能。)
大约

4

在静态和动态类型的语言之间需要权衡取舍。静态类型为编译器提供了大量信息,从而可以对程序(部分)的安全性做出相当有力的保证。但是,这是有代价的,因为您不仅需要知道您的程序是正确的,而且还必须编写适当的代码以使编译器确信这种情况。换句话说,提出索赔要比证明它们容易。

有“不安全”的构造可以帮助您对编译器进行未经证实的断言。例如,无条件调用Nullable.Value,无条件下调,dynamic对象等。它们使您可以声明要求(“我断言对象a是a String,如果我错了,则抛出InvalidCastException”),无需证明它。在证明它比值得证明困难得多的情况下,这很有用。

滥用它是有风险的,这正是为什么存在明显的向下标记表示法并且是强制性的原因。它是一种语法盐,旨在引起人们对不安全操作的注意。该语言本可以通过隐式向下转换来实现的(其中推断的类型是明确的),但这将隐藏这种不安全的操作,这是不希望的。


我想我想问的是一个例子,说明在什么地方/何时何地或何时需要(例如良好实践)做出这种“未经证实的断言”。
ChrisW '18

1
如何转换反射操作的结果?stackoverflow.com/a/3255716/3141234
亚历山大

3

向下转换被认为是不好的,原因有两个。我主要认为是因为它是反面向对象的。

OOP真的很喜欢它,如果您再也不需要丢脸的话,它的“存在理由”就是多态意味着您不必走下去

if(object is X)
{
    //do the thing we do with X's
}
else
{
    //of the thing we do with Y's
}

你只是做

x.DoThing()

并且代码自动神奇地完成了正确的事情。

有一些不容忽视的“硬”原因:

  • 在C ++中,它很慢。
  • 如果选择错误的类型,则会出现运行时错误。

但是在某些情况下替代向下转换的方法可能非常丑陋。

经典示例是消息处理,您不想在对象上添加处理功能,而是将其保留在消息处理器中。然后MessageBaseClass,我要在阵列中处理一吨,但我还需要每个子类型按正确处理。

您可以使用Double Dispatch来解决问题(https://en.wikipedia.org/wiki/Double_dispatch)...但这也有问题。或者,您也可以因为一些困难的原因而放弃一些简单的代码。

但这是在泛型发明之前。现在,您可以通过提供一种在以后指定详细信息的类型来避免向下转换。

MessageProccesor<T> 可以按指定类型更改其方法参数和返回值,但仍提供通用功能

当然,没有人会强迫您编写OOP代码,并且提供了许多语言功能,但令人反感,例如反射。


1
我怀疑在C ++中它运行缓慢,尤其是如果您使用static_cast而不是dynamic_cast
ChrisW '18

“消息处理”-我认为这是含义,例如转换lParam为特定的内容。不过,我不确定这是否是一个很好的C#示例。
ChrisW '18

3
@ChrisW-不错,是的,static_cast它总是很快...但是如果您犯了一个错误并且类型不是您所期望的,则不能保证不会以未定义的方式失败。
朱尔斯

@Jules:完全不对。
Mehrdad

@Mehrdad,这正是重点。在C ++中执行等效的C#转换很慢。这是反对铸造的传统原因。备用static_cast不等效,由于行为未定义,因此被认为是较差的选择
Ewan

0

除了前面提到的所有内容外,还可以想象一个对象类型为Tag的属性。它为您提供了一种将所选对象存储在另一个对象中,以便以后根据需要使用的方法。您需要将此属性下调。

总的来说,直到今天不使用某些东西并不总是表明某些东西无用;-)


是的,例如参见.net中的Tag属性有什么用。有人写道,在其中一个答案中,我曾经用它在Windows Forms应用程序中向用户输入指令。当控件GotFocus事件触发时,指令Label.Text属性被分配了包含指令字符串的控件Tag属性的值。我猜想“扩展” Control(而不是使用object Tag属性)的另一种方法是创建一个Dictionary<Control, string>存储每个控件标签的容器。
ChrisW '18

1
我喜欢这个答案,因为它Tag是.Net框架类的公共属性,它表明您(或多或少)应该使用它。
ChrisW '18

@ChrisW:并不是说结论是错误的(或正确的),而是要注意这是草率的推理,因为有很多公共的.NET功能已经过时(比如说System.Collections.ArrayList)或不推荐使用(比如说IEnumerator.Reset)。
Mehrdad

0

在我的作品中,垂头丧气最常见的用法是由于某些白痴违反了Liskov的“替代原则”。

想象一下某个第三方库中有一个接口。

public interface Foo
{
     void SayHello();
     void SayGoodbye();
 }

还有两个实现它的类。BarQuxBar表现得很好。

public class Bar
{
    void SayHello()
    {
        Console.WriteLine(“Bar says hello.”);
    }
    void SayGoodbye()
    {
         Console.WriteLine(“Bar says goodbye”);
    }
}

但是Qux表现不佳。

public class Qux
{
    void SayHello()
    {
        Console.WriteLine(“Qux says hello.”);
    }
    void SayGoodbye()
    {
        throw new NotImplementedException();
    }
}

好吧...现在我别无选择。为了避免程序崩溃,我必须输入check和(可能)向下转换。


1
对于此特定示例,不是正确的。您可以在调用SayGoodbye时捕获NotImplementedException。
Per von Zweigbergk

也许这是一个过于简化的示例,但是稍微使用一些较旧的.Net API,您就会遇到这种情况。有时,编写代码的最简单方法是安全地转换ala var qux = foo as Qux;
RubberDuck

0

另一个真实示例是WPF的VisualTreeHelper。它使用向下转换来投射DependencyObjectVisualVisual3D。例如,VisualTreeHelper.GetParent。失败发生在VisualTreeUtils.AsVisualHelper中

private static bool AsVisualHelper(DependencyObject element, out Visual visual, out Visual3D visual3D)
{
    Visual elementAsVisual = element as Visual;

    if (elementAsVisual != null)
    {
        visual = elementAsVisual;
        visual3D = null;
        return true;
    }

    Visual3D elementAsVisual3D = element as Visual3D;

    if (elementAsVisual3D != null)
    {
        visual = null;
        visual3D = elementAsVisual3D;
        return true;
    }            

    visual = null;
    visual3D = null;
    return false;
}
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.