无法从父类转换为子类进行强制类型转换

135

我试图将一个父类强制转换为子类,但是却收到了一个InvalidCastException的异常。子类只有一个int类型的属性。有人知道我需要做什么吗?


还有一点需要知道的是,你不能对基类/派生类使用显式转换。 - Rzassar
13个回答

194

在C#中进行向下转型的简单方法是将父类进行序列化,然后将其反序列化为子类。

 var serializedParent = JsonConvert.SerializeObject(parentInstance); 
 Child c  = JsonConvert.DeserializeObject<Child>(serializedParent);

我有一个简单的控制台应用程序,使用上面两行代码将动物转换为狗,参考这里


36
嗯,我不太敢称之为“沮丧”。 - Kirk Woll
12
我喜欢当有人能够跳出传统思维定式,制止那些告诉原帖作者他做不到的人(除了一两个恶意评论者)!感谢你在这件事上的帮助,我前几个小时一直试图弄清楚这个问题 :) - Derek Foulk
7
这是一个很好的解决方案。我有一个情况,我的子类只是一个父类的包装器,没有额外的功能。我这样做是因为我不想在我的应用程序中导入 Web 引用,因为它已经在我的辅助库中了。这使我能够将父类转换为我的包装器类。谢谢! - Mageician
2
你是个天才! :) - Yablargo
6
我想知道这种序列化方式相对于创建派生类型的新实例并将其属性从基类属性中赋值会慢多少倍。 - bytedev
显示剩余8条评论

153

你不能把哺乳动物强制变成一只狗 - 它可能是只猫。

你不能把食物强行变成三明治 - 它可能会是个芝士汉堡。

你不能把一辆车强行变成法拉利 - 它可能是本田,更具体地说,你不能把法拉利360 Modena强制转换为法拉利360 Challange Stradale - 它们有不同的部件,尽管它们都是法拉利360。


24
可理解的障碍使得以这种方式实际上无法 "复制"。但是如果他想要一只与哺乳动物对象中所持有的猫拥有相同眼色/体重/毛发花纹/年龄等共同属性的狗呢?基本上是在复制这些共同属性。 - FastAl
9
FastAl,这正是我们需要接口的原因。Mammal必须实现IMammal接口并包含眼睛颜色、体重等属性。现在你可以将狗和猫都转换为IMammal接口类型。 - Tom Deloford
1
你可以将哺乳动物强制转换为狗。如果它是一只狗,那么它就是一只狗。否则,它会变成null。如果猫有这些重载函数允许这样做,“overload”函数可以使从猫到狗的不可能转换成为可能。但是,你的工作是处理数据丢失并适应不存在的数据。比如将爪子转换成指甲,追逐绳子转换成追逐球等等... - TamusJRoyce
1
法拉利比喻 很好 - Lord Darth Vader
1
当您使用继承作为特化时,这变得不太明显。例如:只有一条继承线而不是树形结构。例如:SuperHero是Hero的子类->没有其他类可以成为Hero的子类,但对于SuperHero来说,成为Hero的子类很有用,因为它可以拥有其他能力和特征。当他获得新的力量时,将标准英雄升级为超级英雄可能很方便。 - Sébastien
显示剩余2条评论

62

你的基类引用所指向的实例不是你子类的实例。这没有问题。

更具体地说:

Base derivedInstance = new Derived();
Base baseInstance = new Base();

Derived good = (Derived)derivedInstance; // OK
Derived fail = (Derived)baseInstance; // Throws InvalidCastException

为了使强制类型转换成功,你需要将要进行强制类型转换的实例(downcasting)确保是你所要转换成的类的实例(或者至少是该类的继承链中的一个),否则强制类型转换会失败。


或者潜在的Base otherDerived = new OtherDerived(); Derived otherFail = (Derived)otherDerived; - Blair Conrad
基类 Base { } 派生类 Derived : Base { }//在主方法中 Base derivedInstance = new Derived(); Base baseInstance = new Base(); Derived good = (Derived)derivedInstance; Derived fail = (Derived)baseInstance; 这段代码在 .NET 3.5 中可以编译通过,你说的问题在哪里? - pradeeptp
9
当然会构建。谁说有编译错误了? - Greg D

29

我看到很多人说显式的父子类型转换是不可能的,但实际上这并不是真的。让我们重新开始,并尝试通过示例来证明它。

正如我们所知,在.NET中,所有转换都有两个广泛的类别。

  1. 对于值类型
  2. 对于引用类型(在您的情况下是引用类型)

引用类型进一步包含三种主要情况,任何场景都可以属于其中之一。

子类型向父类型转换(隐式类型转换 - 总是成功的)

情况1. 子类型到任何直接或间接父类型的转换。

Employee e = new Employee();
Person p = (Person)e; //Allowed

从父类到子类的转换(显式转换- 可以成功)

情况2. 父类变量持有父类对象(不允许)

Person p = new Person();  // p is true Person object
Employee e = (Employee)p; //Runtime err : InvalidCastException <-------- Yours issue

案例3. 父变量持有子对象 (始终成功)

注意: 因为对象具有多态性质,所以父类类型的变量可以持有子类型。

Person p = new Employee(); // p actually is Employee
Employee e = (Employee)p; // Casting allowed

结论: 在阅读以上内容后,希望现在能理解像父母如何将子类转换(Case 3)。

问题的答案:

你的答案在Case 2中。在那里,您可以看到OOP不允许这样的类型转换,并且您正在尝试违反OOP的基本规则。因此,请始终选择安全路径。

此外,为了避免这种异常情况,.net建议使用is/as运算符,这些运算符将帮助您做出明智的决策并提供安全转换。


我喜欢你的回答很清晰。唯一的问题是is/as文档的链接出现了404错误。以下链接是否是你所引用的内容? 类型测试和转换 - J Man

20

在一些情况下,这样的转换是有意义的。
就我个人而言,我从网络上接收到了一个基类,并且需要为其添加更多功能。因此,在我的端上派生出一个包含所有我想要的特性的派生类,并将接收到的基类转换为派生类并不是一个选项(当然会抛出 InvalidCastException 异常)

一个实用的“开箱即用”解决方案是声明一个扩展 Helper 类,它实际上并没有继承基类,而是将其作为成员包含进来。

public class BaseExtension
{
   Base baseInstance;

   public FakeDerived(Base b)
   {
      baseInstance = b;
   }

   //Helper methods and extensions to Base class added here
}

如果您的代码松耦合,并且只需要向基类添加一些额外特性,而不是真正需要继承,那么这可能是一个快速简单的解决方法。


我想知道你是否希望你的 BaseExtension 至少实现 IBase,以便在类似的上下文中使用它?或者这对你的需求不重要? - tobriand
有时候,包含关系可以作为继承的适当替代。 - Vahid Ghadiri

15

那样会违反面向对象的原则。我认为在这里和项目的其他地方,一个优雅的解决方案是使用对象映射框架,比如AutoMapper来配置投影。

这是一个稍微复杂一些的配置,但对于大多数情况而言足够灵活:

public class BaseToChildMappingProfile : Profile
{
    public override string ProfileName
    {
        get { return "BaseToChildMappingProfile"; }
    }

    protected override void Configure()
    {
        Mapper.CreateMap<BaseClass, ChildClassOne>();
        Mapper.CreateMap<BaseClass, ChildClassTwo>();
    }
}


public class AutoMapperConfiguration
{
    public static void Configure()
    {
        Mapper.Initialize(x =>
        {
            x.AddProfile<BaseToChildMappingProfile>();
        });
    }
}
当应用程序启动时调用 AutoMapperConfiguration.Configure(),然后您可以像这样进行投影:
ChildClassOne child = Mapper.Map<BaseClass, ChildClassOne>(baseClass);

属性是按照惯例映射的,因此如果类被继承,则属性名称完全相同,并且映射会自动配置。您可以通过调整配置来添加其他属性。请参见文档


使用Automapper将具有单个属性的类型映射到另一个类型(就像OP所描述的那样)就像用大锤砸蛋一样。为什么不只是新建派生类型并自己分配其属性(这只需要1行代码)。 - bytedev

9

保罗,你没有问“我能做到吗”——我假设你想知道如何做到!

我们在一个项目中不得不这样做——我们设置了许多类,以一种通用的方式进行一次设置,然后初始化特定于派生类的属性。我使用VB,所以我的示例是VB(很难受),但我从这个网站窃取了VB示例,该网站还有更好的C#版本:

http://www.eggheadcafe.com/tutorials/aspnet/a4264125-fcb0-4757-9d78-ff541dfbcb56/net-reflection--copy-cl.aspx

示例代码:

Imports System
Imports System.Collections.Generic
Imports System.Reflection
Imports System.Text
Imports System.Diagnostics

Module ClassUtils

    Public Sub CopyProperties(ByVal dst As Object, ByVal src As Object)
        Dim srcProperties() As PropertyInfo = src.GetType.GetProperties
        Dim dstType = dst.GetType

        If srcProperties Is Nothing Or dstType.GetProperties Is Nothing Then
            Return
        End If

        For Each srcProperty As PropertyInfo In srcProperties
            Dim dstProperty As PropertyInfo = dstType.GetProperty(srcProperty.Name)

            If dstProperty IsNot Nothing Then
                If dstProperty.PropertyType.IsAssignableFrom(srcProperty.PropertyType) = True Then
                    dstProperty.SetValue(dst, srcProperty.GetValue(src, Nothing), Nothing)
                End If
            End If
        Next
    End Sub
End Module


Module Module1
    Class base_class
        Dim _bval As Integer
        Public Property bval() As Integer
            Get
                Return _bval
            End Get
            Set(ByVal value As Integer)
                _bval = value
            End Set
        End Property
    End Class
    Class derived_class
        Inherits base_class
        Public _dval As Integer
        Public Property dval() As Integer
            Get
                Return _dval
            End Get
            Set(ByVal value As Integer)
                _dval = value
            End Set
        End Property
    End Class
    Sub Main()
        ' NARROWING CONVERSION TEST
        Dim b As New base_class
        b.bval = 10
        Dim d As derived_class
        'd = CType(b, derived_class) ' invalidcast exception 
        'd = DirectCast(b, derived_class) ' invalidcast exception
        'd = TryCast(b, derived_class) ' returns 'nothing' for c
        d = New derived_class
        CopyProperties(d, b)
        d.dval = 20
        Console.WriteLine(b.bval)
        Console.WriteLine(d.bval)
        Console.WriteLine(d.dval)
        Console.ReadLine()
    End Sub
End Module

当然,这并不是真正的转换。它是创建一个新的派生对象并从父对象中复制属性,将子属性留空。这就是我需要做的,听起来这也是你需要做的。请注意,它仅复制属性,而不是类中的成员(公共变量)(但如果您暴露公共成员,则可以扩展它以执行此操作)。
通常情况下,类型转换会创建指向同一对象的2个变量(在这里进行迷你教程,请不要对我提出特殊情况的异常)。这有重大影响(留给读者练习)!
当然,我必须说一下为什么语言不允许从基实例到派生实例,但反过来可以。想象一种情况,您可以取得Winforms文本框(派生)的实例,并将其存储在类型为Winforms控件的变量中。当然,“控件”可以很好地移动对象,并且您可以处理有关文本框的所有“控件层面”的内容(例如,top,left,.text属性)。文本框特定的内容(例如,.multiline)无法在不将指向内存中文本框的“控件”类型变量转换的情况下看到,但它仍然存在于内存中。
现在想象一下,你有一个控件,并且你想将一个文本框类型的变量转换为它。内存中的控件缺少“多行”和其他文本框特性。如果您尝试引用它们,控件不会自动增加多行属性!该属性(在此将其视为成员变量,实际上存储值-因为文本框实例的内存中存在一个值)必须存在。由于您正在进行强制转换,请记住,它必须是您指向的同一对象。因此,这不是语言限制,而是在这种方式下进行强制转换在哲学上是不可能的。

1
我知道这已经晚了,但你应该在“如果dstProperty不为空”的测试中包括“AndAlso dstProperty.CanWrite”,以确保它不是只读属性。 - JamesMLV
@JamesMLV - 谢谢你的好发现。'事后' - 看起来OP不打算接受任何答案 :-( 所以也就没有什么事实可言了。哎。 - FastAl

4

对象的实例应该使用子类的类型创建,你不能将父类型的实例转换为子类型


3
截至C# 7.0,您可以使用is关键字来执行以下操作:
有了这些类定义:
class Base { /* Define base class */ }
class Derived : Base { /* Define derived class */ }

然后您可以执行类似以下操作:

void Funtion(Base b)
{
    if (b is Derived d)
    {
        /* Do something with d which is now a variable of type Derived */
    }
}

这将等同于:

void Funtion(Base b)
{
    Defined d;
    if (b is Derived)
    {
        d = (Defined)b;
        /* Do something with d */
    }
}

现在你可以调用:

Function(new Derived()); // Will execute code defined in if

以及

Function(new Base()); // Won't execute code defined in if

这样你就可以确保你的降级将是有效的,不会抛出异常!


你实际上并没有进行向下转型,如果你使用了"new Base()",那才算是向下转型,正如你所评论的"它不会执行"。发送一个新的Derived()基本上是调用一个接收父类参数的方法,然后检查派生类的类型...所以这种方法是行不通的。 - Yogurtu
在这里,你实际上并没有进行向下转型,如果你使用了"new Base()",那才算是向下转型,正如你所评论的"它不会执行"。发送一个新的Derived()基本上是调用一个接收父类的方法,然后检查派生类的类型...所以这种方法是行不通的。 - undefined

2

对我来说,只需将基类的所有属性字段复制到父类中即可,如下所示:

using System.Reflection;

public static ChildClass Clone(BaseClass b)
{
    ChildClass p = new ChildClass(...);

    // Getting properties of base class

    PropertyInfo[] properties = typeof(BaseClass).GetProperties();

    // Copy all properties to parent class

    foreach (PropertyInfo pi in properties)
    {
        if (pi.CanWrite)
            pi.SetValue(p, pi.GetValue(b, null), null);
    }

    return p;
}

可以在这里找到适用于任何对象的通用解决方案。


这对于引用类型属性不起作用。新实例和旧实例将使用相同的引用来处理引用类型属性。 - Ebram Shehata
只适用于没有嵌套属性、列表或两者同时存在的情况。 - Yogurtu
在没有嵌套属性、列表或两者同时存在的情况下,这个方法是可行的。 - undefined

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