C# 4.0:动态转静态类型

12
这是一个与我之前提问(链接)相关的问题。我将其拆分为子问题:
我在将一个类型为dynamic的对象转换为另一个(已知的)静态类型时遇到了困难。
我有一个ironPython脚本正在执行此操作:
import clr
clr.AddReference("System")
from System import *

def GetBclUri():
    return Uri("http://google.com")

请注意,这只是创建一个BCL System.Uri类型对象并返回它。因此,我知道返回对象的静态类型。
现在在C#中,我正在创建脚本托管内容,并调用此getter以返回Uri对象:
dynamic uri = scriptEngine.GetBclUri();
System.Uri u = uri as System.Uri; // casts the dynamic to static fine

工作没有问题。现在我可以像静态实例化一样使用强类型的Uri对象。

然而......

现在我想定义自己的C#类,在动态领域中进行新建,就像我对Uri所做的那样。我的简单C#类:

namespace Entity
{
    public class TestPy // stupid simple test class of my own
    {
        public string DoSomething(string something)
        {
            return something;
        }
    }
}

现在在Python中,创建一个这种类型的对象并返回它:
sys.path.append(r'C:..path here...')
clr.AddReferenceToFile("entity.dll")
import Entity.TestPy

def GetTest():
    return Entity.TestPy(); // the C# class

然后在C#中调用getter:

dynamic test = scriptEngine.GetTest();
Entity.TestPy t = test  as Entity.TestPy; // t==null!!!

这里,强制类型转换不起作用。请注意,“test”对象(动态类型)是有效的——我可以调用DoSomething()方法——但它无法转换为已知的静态类型

string s = test.DoSomething("asdf"); // dynamic object works fine

我很困惑。BCL类型System.Uri可以从动态类型转换为正确的静态类型,但我的自定义类型却不行。显然,我对此有所误解...

--

更新:我进行了一系列测试,以确保我的程序集引用都正确地对齐。我更改了引用的程序集版本号,然后查看了C#中dynamic对象的GetType()信息 - 它是正确的版本号,但它仍然无法正确地转换回已知的静态类型。

然后,我在我的控制台应用程序中创建了另一个类来检查是否会得到相同的结果,结果是肯定的:我可以在C#中获取到一个dynamic引用,该引用实例化在我的Python脚本中的静态类型,但它无法正确地转换回已知的静态类型。

--

更多信息:

Anton在下面建议AppDomain程序集绑定上下文很可能是罪魁祸首。经过一些测试,我认为这很可能是原因...但我无法弄清楚如何解决它!我之前不知道程序集绑定上下文,所以感谢Anton让我更加了解程序集解析和其中出现的微妙错误。

因此,我在启动脚本引擎之前在C#中放置了一个事件处理程序,以观察程序集解析过程。这使我能够看到Python引擎启动和运行时开始解析程序集:

private static Type pType = null; // this will be the python type ref

// prior to script engine starting, start monitoring assembly resolution
AppDomain.CurrentDomain.AssemblyResolve 
            += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

...并且处理程序将变量pType设置为Python正在加载的类型

static void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
{

    if (args.LoadedAssembly.FullName == 
        "Entity, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null")
    {
        // when the script engine loads the entity assembly, get a reference
        // to that type so we can use it to cast to later.
        // This Type ref magically carries with it (invisibly as far as I can 
        // tell) the assembly binding context
        pType = args.LoadedAssembly.GetType("Entity.TestPy");
    }
}

因此,虽然Python使用的类型与C#中的类型相同,但我认为(如Anton所提出的),不同的绑定上下文意味着对于运行时来说,这两种类型(在“load binding context”和“loadfrom binding context”中的类型)是不同的 - 因此您不能将其转换为另一个。

现在,既然我已经获得了Python加载的Type(以及它的绑定上下文),那么在C#中,我可以将动态对象强制转换为这个静态类型,并且它有效:

dynamic test = scriptEngine.GetTest();
var pythonBoundContextObject = 
       Convert.ChangeType(test, pType); // pType = python bound

string wow = pythonBoundContextObject .DoSomething("success");

但是,唉,这并不能完全解决问题,因为变量 pythonBoundContextObject 虽然类型正确,但仍然带有错误程序集绑定上下文的污点。这意味着我无法将其传递给代码的其他部分,因为我们仍然存在奇怪的类型不匹配,其中绑定上下文的隐形鬼魂使我停滞不前。
// class that takes type TestPy in the ctor... 
public class Foo
{
    TestPy tp;

    public Foo(TestPy t)
    {
        this.tp = t;
    }
}

// can't pass the pythonBoundContextObject (from above): wrong binding context
Foo f = new Foo(pythonBoundContextObject); // all aboard the fail boat

所以解决方案必须在Python端进行:让脚本在正确的程序集绑定上下文中加载。

在Python中,如果我这样做:

# in my python script
AppDomain.CurrentDomain.Load(
    "Entity, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null");

运行时无法解析我的类型:

import Entity.TestPy #fails

请注意[AssemblyVersion] - Hans Passant
看起来你的铁Python和C#正在使用不同版本的卫星程序集的DLL。 - Lucero
dynamic对象的类型是正确的(通过GetType()确认)。我甚至改变了测试程序集的程序集版本号并重新运行它。再次查看dynamic对象的类型信息,它显示了新的程序集版本号——但它仍然无法正确转换。 - Kevin Won
这可能是一个愚蠢的问题,但你是否尝试在向所需类型进行向下转换之前将其向上转换为“object”? - Steven Sudit
Steven:不起作用。我认为Anton是正确的——问题可能是汇编加载到Python的“neither”上下文中,这与它加载到C#(其中它将在“load context”中)的上下文不同。请参见Suzanne Cook的解释。因此,由于上下文的差异,类型不匹配,因此强制转换无法工作。 - Kevin Won
2个回答

3

+1. 非常感谢!执行 <code>engine.Runtime.LoadAssembly(typeof (WidgetEntities.Widget).Assembly);</ code> 可行!我对解决方案唯一的问题是,要概括使用(即,我希望能够拥有可重用的托管组件)意味着我必须了解 C# 中 IronPython 可能引用的所有可能内容,这基本上意味着我必须加载所有内容。 不好。 - Kevin Won

2
我猜 IronPython 会将你的 entity.dll 加载到不同的 程序集加载上下文 中,因此你会加载两个副本,并且其中的类型当然是不同的。你可以通过挂钩 AppDomain.AssemblyResolve / AppDomain.AssemblyLoad 并在 IronPython 尝试加载它时返回你的本地程序集(typeof(Entity.TestPy).Assembly)来解决这个问题,但我不能保证这会起作用。
你不会在 System.Uri 中遇到这个问题,因为运行时对 mscorlib.dll(和可能一些其他系统程序集)进行了特殊处理。 更新:IronPython FAQ 指出,如果程序集尚未加载,则 clr.AddReferenceToFile 使用 Assembly.LoadFile,该方法会加载到“Neither”上下文中。在调用 IronPython 之前访问 Entity.TestPy 的方法以将程序集加载到默认的 Load 上下文中。

(1) 在C#中,我在newing up脚本引擎之前实例化了Entiteis.TestPy类...但没有成功。 (2) AssemblyResolve事件只对'System'触发...它从未对'Entity'触发。我认为你走在正确的轨道上,但是... - Kevin Won
尝试在Python中执行Assembly.Load(...FQ程序集名称...)。该行将被执行,但当我尝试导入Entities.Test时,它找不到它。似乎Python不知道加载绑定上下文。 - Kevin Won
抱歉,我的意思是挂钩 AppDomain.AssemblyLoad - Anton Tykhyy

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