【问题标题】:Explicitly cast base class to derived class将基类显式转换为派生类
【发布时间】:2017-07-18 08:49:54
【问题描述】:

问题:我有 2 种类型,它们是 DB 中 2 个不同过程的结果集: Proc1Result, Proc2Result(我们不得不将它们分开 - 但它们与输入/输出基本相同)

然后我决定我可以使用一个接口在运行时在所需的过程之间切换 - 但这意味着我需要 1 个可以将 Proc1Result 和 Proc2Result 转换为的通用类型

所以我不需要维护这个新类(创建所有属性,如果 DB 结果发生任何变化,则添加/删除) - 我从其中一个结果派生了这个类:

public class DerivedClassForInterface : Proc1Result {}

然后我从第二个 proc 实现了显式转换,它工作正常,但是当我想实现从基类到派生类的显式转换时 - 它不允许我(因为它有点已经“可以” - 但它在运行时失败) :

public class DerivedClassForInterface : Proc1Result 
{
    //ok - and works as expected
    public static explicit operator DerivedClassForInterface(Proc2Result v)
    {
        return new DerivedClassForInterface
        {
            ...
        };
    }

    //fail: 'user-defined' conversations to or from a base class are not allowed
    public static explicit operator DerivedClassForInterface(Proc1Result v)
    {
        return new DerivedClassForInterface
        {
            ...
        };
    }
}

所以这行得通:

//result2 is of type Proc1Result
DerivedClassForInterface castedResult = (DerivedClassForInterface)result2;
//compiles - works as expected at runtime

但事实并非如此:

//result1 is of type Proc1Result
DerivedClassForInterface castedResult = (DerivedClassForInterface)result1;
//compiles - conversation fails at runtime

如果不能从基类转换为派生类,为什么我不能编写自己的显式运算符?

有趣的是,编译器允许我从基类转换为派生类,但它在运行时不起作用。

可能我只会使用简单的功能来为我进行“铸造”。任何人都可以提出更好的解决方案(请记住,我希望保留“DerivedClassForInterface”以遵守“Proc1Result”(或“Proc2Result”的更改 - 没关系))

编辑
@Peter Duniho - 这里的类型“Proc1Result”和“Proc2Result”是作为存储过程 (linq2sql) 的结果生成的。我想要一个代码,当这些程序的输出发生变化时我不需要触摸它(因为我们需要分割一堆程序 - 并且实施新模块可以而且经常确实会增加更多输出)。

Proc1 和 Proc2 基本上是相同的存储过程(它们需要完全相同的输入并提供相同的输出(类型而不是数据))。它们都处理不同的数据段,并且需要分开。

抱歉让这个混乱(在我的工作日结束时......)并且没有澄清 - 这里的问题实际上是:

当运行时导致异常时,为什么编译器允许我从基类转换为派生类?以及为什么我不能自己实现这个转换(......因为它已经实现了 - 但它只是在运行时不起作用?)

所以从我的立场来看 - 它看起来如下:
- 我无法实现这个演员,因为它已经存在
- 但它注定行不通

这里是“最小、完整和可验证的代码示例”(感谢链接):

//results from stored procedures in database which got splitted appart (linq 2 sql)
class Proc1Result { }
class Proc2Result { }
//

class DerivedClassForInterface : Proc1Result
{
    public static explicit operator DerivedClassForInterface(Proc2Result v)
    {
        //this part would be exported in generic function
        var derivedClassInstance = new DerivedClassForInterface();
        var properties = v.GetType().GetProperties();
        foreach (var property in properties)
        {
            var propToSet = derivedClassInstance.GetType().GetProperty(property.Name);
            if (propToSet.SetMethod != null) propToSet.SetValue(derivedClassInstance, property.GetValue(v));
        }
        return derivedClassInstance;
    }
}

interface IProcLauncher
{
    DerivedClassForInterface GetNeededData();
}

class ProcLauncher1 : IProcLauncher
{
    public DerivedClassForInterface GetNeededData()
    {
        var dataFromDb = new Proc1Result();/*just ilustrative*/
        return (DerivedClassForInterface)dataFromDb;
    }
}

class ProcLauncher2 : IProcLauncher
{
    public DerivedClassForInterface GetNeededData()
    {
        var dataFromDb = new Proc2Result();/*just ilustrative*/
        return (DerivedClassForInterface)dataFromDb;
    }
}


class Program
{
    static void Main(string[] args)
    {
        bool causeInvalidCastException = true;

        IProcLauncher procedureLauncher;
        if (causeInvalidCastException) procedureLauncher = new ProcLauncher1();
        else procedureLauncher = new ProcLauncher2();

        var result = procedureLauncher.GetNeededData();
        Console.WriteLine("I got here!");
    }
}

这个想法是:
- 如果程序的输出发生变化,则不必更改任何代码。
- 在运行时决定使用哪个 proc。
- 将转换部分导出为通用函数。
- 必须是可注射的。

我可以解决这个问题 - 比如说 - 只需 1 个通用函数,它可以处理所有情况下的对话,但问题在上面以粗体显示。

【问题讨论】:

  • Proc2Result 在哪里定义?我只看到Proc1Result
  • 我对你的问题有点迷茫,但如果我正确理解了你的问题 - 为什么不将 Proc1Result 和 Proc2Result 之间的所有公共字段复制到一个公共界面。你能告诉你哪些类可以修改,哪些不能。你也可以用一些代码来演示你的问题(不是使用 cast 运算符的解决方案)
  • @rory.ap - Proc1Result 和 Proc2Result 由 Link2Sql 生成,作为存储过程的结果。最初的要求是将他们在 2 个不同的过程中(不是在 1 个过程中)中所做的事情进行细分。最初我想通过强制转换来做到这一点(出于某种原因,编译器确实允许我将基类的实例 obj 强制转换为派生 - 但在运行时它失败了)。我可以通过 2,3.. 等更多方式解决这个问题——但我想知道为什么编译器允许我这样做——但运行时却没有。
  • @Vikhram - “为什么不将 Proc1Result 和 Proc2Result 之间的所有公共字段复制到一个公共接口”。 - 你可能是指属性。我编辑了原始帖子以澄清案例并添加代码来测试它。问题更多是关于 - 为什么它编译,但注定无法工作

标签: c# inheritance interface casting


【解决方案1】:

我不太明白你的问题。您似乎说编译器允许您编写发布的代码,但它在运行时失败。这不是我的经验。基类的显式转换操作出现编译时错误:

错误 CS0553:“Derived.explicit operator Derived(Base1)”:不允许用户定义的与基类之间的转换

对我来说似乎很清楚。至于为什么不允许你写那样的代码,你得问问语言设计者才能确定,但​​这对我来说似乎是一个合理的限制。只要基类实例实际上是派生类的实例,就已经存在从任何基类到该基类的派生类的安全的内置转换。如果允许程序员进行额外的转换,那将是令人困惑的并且可能导致错误,更不用说使语言规范的强制转换/转换运算符规则变得非常复杂。

至于更广泛的问题,我不明白您选择的方法。您正在设计的类与通常这样做的方式完全相反。如果您有许多类都具有共享成员,您希望能够在某些上下文中将所有这些类视为相同,并且您希望能够只实现这些共享成员一次并在其他类之间共享它们,您可以将所有这些成员放在一个基类中,然后从该类中派生所有各种类型。

我什至不明白您当前的方法如何解决这个问题:

这样我就不需要维护这个新类(创建所有属性,如果数据库结果有任何变化,则添加/删除)

由于Proc2Result 不继承Proc1Result,如果Proc1Result 发生更改,则无论如何您都必须更改Proc2Result 才能匹配。以及任何其他类似类型。还有DerivedClassForInterface 类。而且您必须更改所有显式运算符。怎么样更好?

我想你会更喜欢这样的:

class BaseClassForInterface
{
    // declare all shared members here
}

class Proc1Result : BaseClassForInterface { ... }
class Proc2Result : BaseClassForInterface { ... }

然后,对于每个新的Proc...Result 类,您只需继承基类,无需重新编写成员,每个Proc...Result 类的转换是微不足道的。您甚至不需要使用强制转换/转换运算符;该语言已经知道如何从派生类隐式转换为基类,因为派生类基类。

事实上,这是使用 OOP 的标准方式。它是任何 OOP 语言最基本的特性之一。

如果这不能让你回到正轨,你需要改进问题,以便更清楚你在做什么以及为什么。您还需要提供一个很好的Minimal, Complete, and Verifiable code example,清楚地说明您的问题,准确解释该代码的作用以及您希望它做什么。

附录:

感谢您的编辑。您的问题现在更加具体和清晰。我还有疑问,但至少我了解了真实的背景。

在我看来,您已经了解了您问题的大部分基本答案:

当运行时导致异常时,为什么编译器允许我从基类转换为派生类?以及为什么我不能自己实现这个转换(......因为它已经实现了 - 但它只是在运行时不起作用?)

所以从我的立场来看 - 它看起来如下:
- 我无法实现这个演员,因为它已经存在
- 但它注定行不通

即是的,我相信语言不允许这样做,因为已经有一个内置的演员表,是的,你寻求的确切方法注定行不通。

就这部分而言:

这个想法是:
- 如果程序的输出发生变化,则不必更改任何代码。
- 在运行时决定使用哪个 proc。
- 将转换部分导出为通用函数。
- 必须是可注射的。

如果我理解第一点,这就是你继承一个存储过程类型的原因。这样您就可以免费获得财产声明。对我来说似乎有点 hacky,但我承认我确实理解动机。

据我了解上述第三点以及您在帖子中的陈述,您已经知道如何编写通用方法来进行转换。例如。类似:

DerivedClassForInterface ConvertToClassForInterface<T>(T t)
{
    DerivedClassForInterface result = new DerivedClassForInterface();
    Type resultType = typeof(DerivedClassForInterface);
    PropertyInfo[] properties = typeof(T).GetProperties();

    foreach (var property in properties)
    {
        var propToSet = resultType.GetProperty(property.Name);
        if (propToSet.SetMethod != null)
        {
            propToSet.SetValue(result, property.GetValue(t));
        }
    }
    return result;
}

即本质上是您在显式运算符中显示的代码(带有一些小的清理/优化)。或者,您可能没有从字面上使用“通用”一词,而只是表示“通用”。显然,上述方法很少真正受益于通用方法。您可以像显式运算符一样轻松地在参数上使用GetType()

不幸的是,我不知道标准“必须是可注射的” 是否适合这里。打针,怎么打?你的意思是你想在其他地方注入代码?或者您的意思是代码需要与 AOP 系统兼容,或者应用了其他形式的代码注入

忽略我不理解的那部分,我实际上只是利用编译器和运行时为我完成所有繁重的工作(包括缓存反射的东西,这在你的代码中会很慢)。你可以这样写一个类:

class Wrapper
{
    private dynamic _data;

    public string Value {  get { return _data.Value; } }

    public Wrapper(dynamic data)
    {
        _data = data;
    }
}

考虑到其他几个这样的类:

class Result1
{
    public string Value { get; set; }
}

class Result2
{
    public string Value { get; set; }
}

那么你可以这样使用它:

Result1 r1 = new Result1 { Value = "result 1" };
Result2 r2 = new Result2 { Value = "result 2" };
Wrapper w1 = new Wrapper(r1), w2 = new Wrapper(r2);

Console.WriteLine("w1 result: " + w1.Value);
Console.WriteLine("w2 result: " + w2.Value);

即只需创建Wrapper 的实例,传递相关对象(在您的情况下,这将是从存储过程生成的类型)。当然,缺点是您必须向Wrapper 类型添加属性以匹配您的存储过程。但我不相信这是一件坏事。即使您以某种方式对其进行了安排,以便无需更改其余代码,这也是一项相对较小的维护任务。

而且我怀疑更改存储过程无论如何都需要在代码的其他地方进行更改,以显式引用属性。因为毕竟,如果其余代码对于特定的类成员同样完全不可知论(即一直使用反射),那么您可以将结果对象作为object 类型传递,而不必担心包装器在全部。

【讨论】:

  • 感谢重播。你写的东西都很好,通常我就是这样做的,但这里的 Proc1Result 和 Proc2Result 是从存储过程生成的类。我用更多的解释和可测试的代码编辑了原始帖子。
  • @RUKAclMortality:好的,根据您的其他信息,我试图扩展我的答案。请参阅上面的编辑。
  • @ Peter Duniho - 感谢您的提示。 1)对于“可注入”,我的意思是您需要能够在任何需要的地方注入实现的接口类的实例(传递给函数作为参数 fe) 2)是的 - 使用“通用”我的意思实际上是“通用” -我已经添加了我的实现作为答案 3) 事情是 - 这段代码在一个库中,然后被“分发”并在其他程序中使用。
  • ... 这就是为什么我不想在程序输出发生变化时对其进行任何修改的原因。 4)由于这些程序的输出没有很大的数据集(通常是1组或4-5组(基本上是数据的汇总和基于许多数据库中的数据的各种子句的结果))我想我可以应付反射缺点 - 因为它不会做那么多迭代
  • sry,必须再次澄清。使用“分布式” - 我的意思是这个库被添加到一些项目中作为参考。当引用这个库的人 - 已经实现了他的东西 - 他只需要重新导入 proc 并重建。所以我不需要参与(有点自私——我知道)——他也不需要调查/修改我的代码
【解决方案2】:

我通过以下方式实现了转换:

class BaseConverter
{
    protected T Convert<T, X>(X result)
    {
        var derivedClassInstance = Activator.CreateInstance<T>();
        var derivedType = derivedClassInstance.GetType();

        var properties = result.GetType().GetProperties();
        foreach (var property in properties)
        {
            var propToSet = derivedType.GetProperty(property.Name);
            if (propToSet.SetMethod != null)
            {
                propToSet.SetValue(derivedClassInstance, property.GetValue(result));
            }
        }
        return derivedClassInstance;
    }

    protected List<T> Convert<T, X>(List<X> listResult)
    {
        var derivedList = new List<T>();
        foreach (var r in listResult)
        {
            //can cope with this - since there will not ever be many iterations
            derivedList.Add(Convert<T, X>(r));
        }
        return derivedList;
    }
}

所以接口实现类将从它继承:

class ProcLauncher2 : BaseConverter, IProcLauncher
{
    public DerivedClassForInterface GetNeededData()
    {
        var dataFromDb = new Proc2Result();/*just ilustrative*/
        //usage (works for single result or list if I need a list returned):
        return Convert<DerivedClassForInterface, Proc2Result>(dataFromDb);
    }

    //other methods...
}

然而 - 我不清楚 - 为什么已经从基类转换为派生 - 如果这不起作用。 Imo - 它不应该存在并在编译时抛出错误。

【讨论】:

    猜你喜欢
    • 2017-01-08
    • 2013-11-17
    • 2021-02-18
    • 2011-02-04
    • 2012-09-15
    • 2011-06-30
    相关资源
    最近更新 更多