为什么动态类型可以起作用而强制转换不行?

3

到目前为止,我的猜测是动态类型只是在编译期间“关闭”类型检查,并在动态实例上调用消息时执行类似于类型转换的操作。显然还有其他事情发生了。

附带的NUnit测试用例显示了我的问题:使用动态类型,我只能使用具体子类中可用的方法,但是我不能使用强制转换(导致InvalidCastException)。我宁愿进行强制转换,因为这样可以让我在VS中获得完整的代码完成。

有人能解释一下发生了什么和/或给我一个提示,如何在不必在每个具体子类中重新实现WorkWithAndCreate方法的情况下获得代码完成吗?

谢谢,Johannes

using System;
using NUnit.Framework;

namespace SlidesCode.TestDataBuilder
{
    [TestFixture]
    public class MyTest
    {
        [Test]
        public void DynamicWorks()
        {
            string aString = CreateDynamic(obj => obj.OnlyInConcreteClass());
            Assert.AreEqual("a string", aString);
        }

        private static string CreateDynamic(Action<dynamic> action)
        {
            return new MyConcreteClass().WorkWithAndCreate(action);
        }

        [Test]
        public void CastingDoesNotWorkButThrowsInvalidCastException()
        {
            string aString = CreateWithCast(obj => obj.OnlyInConcreteClass());
            Assert.AreEqual("a string", aString);
        }

        private static string CreateWithCast(Action<MyConcreteClass> action)
        {
            return new MyConcreteClass().WorkWithAndCreate((Action<MyGenericClass<string>>) action);
        }
    }

    internal abstract class MyGenericClass<T>
    {
        public abstract T Create();
        public T WorkWithAndCreate(Action<MyGenericClass<T>> action)
        {
            action(this);
            return this.Create();
        }
    }

    internal class MyConcreteClass : MyGenericClass<string>
    {
        public override string Create()
        {
            return "a string";
        }

        public void OnlyInConcreteClass()
        {
        }
    }
}

以下是我评论中格式化的实际例子:

这里是一个实际例子:

Customer customer = ACustomer(cust =>
        {
            cust.With(new Id(54321));
            cust.With(AnAddress(addr => addr.WithZipCode(22222)));
        });

private static Address AnAddress(Action<AddressBuilder> buildingAction)
{
    return new AddressBuilder().BuildFrom(buildingAction);
}

private static Customer ACustomer(Action<CustomerBuilder> buildingAction)
{
    return new CustomerBuilder().BuildFrom(buildingAction);
}

一些细节缺失,但我希望它能清楚地表达目的。

请提供完整的错误信息。 - Merlyn Morgan-Graham
我认为再提出一个问题来解决你的实际例子的设计考虑是值得的。需要注意措辞(以避免开放式问题),但我很想有一个不仅仅是评论空间的地方来写我的回复 :) - Merlyn Morgan-Graham
2个回答

3

dynamic之所以有效是因为dynamic不依赖于对象类型的编译时知识。 MyGenericClass<string>没有OnlyInConcreteClass()方法,但您传递的实例当然有该方法,dynamic会找到它。

顺便说一句,您可以像这样使WorkWithAndCreate有效:

public T WorkWithAndCreate<T1>(Action<T1> action)
    where T1 : MyGenericClass<T>
{
    action((T1)this);
    return this.Create();
}

那么,调用也会有效:
private static string CreateWithCast(Action<MyConcreteClass> action)
{
    return new MyConcreteClass().WorkWithAndCreate(action);
}

现在你不需要再进行转换了。

关于你的构建器,以下内容是否可行?

private static TResult AnInstance<TBuilder, TResult>(Action<TBuilder> buildingAction)
    where TBuilder : Builder<TResult>, new()
{
    return new TBuilder().BuildFrom(buildingAction);
}

唯一获得代码补全的方法是拥有一个具体类型,而使您的具体类型可以在派生类型中更改的唯一方法是将其编写为通用函数,并带有基础类型约束。 - Merlyn Morgan-Graham
那解决了手头的问题-非常感谢。我仍然很乐意听取有关如何改进生成器语法的建议,但这可能需要提出一个新问题。 - johanneslink

1

这是使用动态的示例:

http://msdn.microsoft.com/en-us/library/dd264736.aspx

你说:

我之前的猜测是动态类型在编译期间“关闭”类型检查,并在调用动态实例上的消息时执行类似于类型转换的操作。

实际上,它使用反射在运行时按名称查找您调用的方法、属性和字段。除非您将对象强制转换回其基础类型,否则不会进行任何转换。

至于您实际的问题,您能给出更具体的示例吗?可能有更好的设计,但您并没有告诉我们您要做什么 - 只是您当前正在做什么。

猜测一下,您可能希望使用基本的 interface,并使所有函数接受该基本接口。然后将您想要在该接口上调用的方法放在其中,并在您的具体类型中实现它们。通常情况下,当您没有基本类型或无法修改基本类型以添加虚拟或抽象方法时,才会使用 dynamic 作为解决方法。

如果您真的想让它按原样工作,您必须使用泛型类型参数而不是动态类型参数来编写它。请参见 Pieter 的解决方案,了解如何正确地执行此操作。


我试图做的是为测试用例构建测试数据提供易于语法的方法。标准模式(来自Java社区)是在构建器对象上具有流畅的接口,并在最后一步调用Build()来实际创建对象。使用lambda代替可以摆脱Build(),但会引入其他挑战 - 特别是如果我想以通用方式创建它。 - johanneslink
@johanneslink:您能提供一些您想要启用的样例语法以及几种不同的场景吗?这样反推会更容易。 "构建测试数据" 可能意味着许多事情。在早期过于泛化您的需求可能是问题所在。 - Merlyn Morgan-Graham
@johanneslink:我发现复制模式和代码几次比试图在得到一些具体要求之后将概念桥接到通用概念更容易。创建一个良好的流畅接口(基本上是特定于领域的语言)已经足够复杂了。 - Merlyn Morgan-Graham
这是一个真实世界的例子:customer = ACustomer(cust => { cust.With(new Id(54321)); cust.With(AnAddress(addr => addr.WithZipCode(22222))); }); private Address AnAddress(Action<AddressBuilder> buildingAction) { return new AddressBuilder().BuildFrom(buildingAction); } private Customer ACustomer(Action buildingAction) { return new CustomerBuilder().BuildFrom(buildingAction); }将BuildFrom泛型化解决了代码完成问题。 - johanneslink
@johanneslink:在这里,你可以采取一些方法来简化这段代码。其中一种方法是使用初始化语法:var customer = new Customer() { Id = new Id(54321), Address = new Address() { ZipCode = 22222 }, };。如果不可用,你可以像 Rhino.Mocks 那样创建通用扩展方法:public static T With<T>(this T instance, Action<T> mutator) { mutator(instance); return instance; },并像这样使用它:var customer = new Customer().With(AnId(12345))。你的 AnId 函数将返回一个委托 - return c => c.Id = new Id(value) - Merlyn Morgan-Graham
@johanneslink:既然你仍然要创建那些特定的私有函数,不如将它们作为扩展方法: public static Customer With(this Customer instance, Action<T> mutator) { /... } 或者甚至是 public static Customer WithId(this Customer instance, int id);. 当你开始将它们添加到所有待测试的类中时,它们可以以相当酷的方式组合在一起: new Customer().WithAddress(Address.Generated().WithZip(54321)). 当然,如果你可以使用初始化语法,那么除非它进行了高级操作(不仅仅是属性设置),否则你不需要太多这样的东西。 - Merlyn Morgan-Graham

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