无法使用涉及多重继承的对象访问基 class 中的正确属性

Can't access the correct properties in base class with object involving multiple inheritance

有些事情我想不通。我有以下代码:

public interface IProject
{
    
}

public class ModuleProject : IProject
{

}

public class AllProject : IProject
{
    public ModuleProject ModuleData => new ModuleProject();
}

public interface IProjectInstance
{
    IProject Data { get; }
}

public interface IModuleProjectInstance : IProjectInstance
{
    new ModuleProject Data { get; }
}


public interface IAllProjectInstance : IModuleProjectInstance
{
}

public class AllProjectInstance : IAllProjectInstance
{
  
    IProject IProjectInstance.Data => Data;

    public AllProject Data => new AllProject();

    ModuleProject IModuleProjectInstance.Data => Data.ModuleData;
}

public abstract class Worker<T,VM>
where T:class, IProject
where VM:class, IProjectInstance
{
    private readonly T _data;
    protected Worker(VM viewModel)
    {
        _data = (T)viewModel.Data;  //fails to cast
    }
}

public class ModuleWorker : Worker<ModuleProject, IModuleProjectInstance>
{

    public ModuleWorker(IModuleProjectInstance viewModel) : base(viewModel)
    {
    }
}

当我运行下面的代码时:

 var projInstance = new AllProjectInstance();
 var moduleWorker = new ModuleWorker(projInstance);

由于 InvalidCastException,程序在 _data = (T)viewModel.Data 崩溃。

System.InvalidCastException HResult=0x80004002 Message=Unable to cast object of type 'ProjectInstance.AllProject' to type 'ProjectInstance.ModuleProject'. Source=ProjectInstance
StackTrace: at ProjectInstance.Worker`2..ctor(VM viewModel) in D:.net Samples project\August 2021\ProjectInstance\Worker.cs:line 15 at ProjectInstance.ModuleWorker..ctor(IModuleProjectInstance viewModel) in D:.net Samples project\August 2021\ProjectInstance\Worker.cs:line 24 at ProjectInstance.Program.Main(String[] args) in D:.net Samples project\August 2021\ProjectInstance\Program.cs:line 16

看来 viewModelAllProjectInstance 类型,AllProjectInstance.DataAllProject 类型,但 TModuleProject ,因此例外。很公平。

但是假设我稍微修改一下代码:

public abstract class Worker<T,VM>
where T:class, IProject
where VM:class, IProjectInstance
{
    private readonly T _data;

    protected Worker(T data) //this works!
    {
        _data = data;
    }

}

public class ModuleWorker : Worker<ModuleProject, IModuleProjectInstance>
{
    public ModuleWorker(IModuleProjectInstance viewModel) : base(viewModel.Data)
    {
    }

}

那就成功了!因为在 ModuleWorker 构造函数中, viewModel 被推断为 IModuleProjectInstance 的类型,因此它是 AllProjectInstance 内部的 ModuleProject IModuleProjectInstance.Data 属性被调用,因此也不例外。

我的问题:

  1. C# 规范中是否记录了此行为?
  2. 这样设计的合理性是什么?
  3. 有没有办法将其转换为基础 class (Worker<T,VM>) 以便推断出正确的 属性?

Worker的构造函数中,viewModel.Data是一个需要执行的member access, and to evaluate this member access, a member lookup。这是 Data 类型 VMviewModel 的类型)的成员查找,具有 0 个类型参数。让我们看看如何根据规范进行成员查找。

(T指的是我们要查找的成员的类型,所以这里=VMN是我们要查找的成员的名称,所以 = Data):

First, a set of accessible members named N is determined:

  • If T is a type parameter, then the set is the union of the sets of accessible members named N in each of the types specified as a primary constraint or secondary constraint for T, along with the set of accessible members named N in object.
  • Otherwise, the set consists of all accessible members named N in T, including inherited members and the accessible members named N in object. [...]

VM是类型参数,VM的约束是class IProjectInstance,所以第一点适用。集合只有属性IProject Data { get; }.

其他步骤仅尝试从集合中删除元素,其中 none 个适用于本例,因此我不会重复这些步骤。最后,IProjectInstance声明的Data属性就是查找的结果

现在,AllProjectInstance中的哪个实现对应于IProjectInstance.Data的实现?当然是IProjectInstance

的显式接口实现
// This returns an AllProject object, causing the cast to fail
IProject IProjectInstance.Data => Data;

另一方面,如果在 ModuleWorker 构造函数中执行 viewModel.Data,则 viewModel 的类型为 IModuleProjectInstance。如果你应用相同的成员查找算法,你会看到这次它查找了IModuleProjectInstance中声明的Data 属性,而AllProjectInstance中对应的实现是:

ModuleProject IModuleProjectInstance.Data => Data.ModuleData;

我不确定您为什么认为这出乎意料。 IMO 函数成员自然不知道调用者调用它们的表达式。如果您希望 viewModel.Data 解析为 IModuleProjectInstance.Data,则需要 Worker 构造函数知道它已使用编译时类型为 IModuleProjectInstance 的表达式调用。也就是说,这在 C# 中并非不可能 - 您可以要求调用者传入一个 Expression<Func<VM>> 并操作表达式树以找出其编译时类型,并获得您想要的 Data反射,但这太过分了。

与其进行转换,不如将 IProjectInstance 设为通用:

public interface IProjectInstance<T> where T: IProject
{
    T Data { get; }
}

public interface IModuleProjectInstance : IProjectInstance<ModuleProject>
{
    
}


public interface IAllProjectInstance : IModuleProjectInstance
{
}

public class AllProjectInstance : IAllProjectInstance, IProjectInstance<AllProject>
{

    public AllProject Data => new AllProject();

    ModuleProject IProjectInstance<ModuleProject>.Data => Data.ModuleData;
}

public abstract class Worker<T,VM>
where T:class, IProject
where VM:class, IProjectInstance<T>
{
    private readonly T _data;
    protected Worker(VM viewModel)
    {
        _data = viewModel.Data;
    }
}

如果实在不行IProjectInstance泛型,也可以尝试用反射获取第二个类型参数的类型,找到Data属性,得到它的价值。

protected Worker(VM viewModel)
{
    var dataProperty = GetType().BaseType.GetGenericArguments()[1].GetProperty("Data");
    if (dataProperty != null) {
        _data = (T)dataProperty.GetValue(viewModel);
    } else { // fall back to your original approach
        _data = (T)viewModel.Data;
    }
}

这里对Worker的子类做了一些假设,比如:

  • 直接超类是Worker
  • VM 确实有一个可访问的 Data 属性 returns 可以转换为 T 的东西。现在,如果 IModuleProjectInstance.Data 被删除,ModuleWorker 仍然可以编译。