C#/.NET 中的动态语言运行时【译】

726 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

原文 Dynamic Language Runtime in C#/.NET

尽管 C# 属于静态类型语言,但是一些动态特性很早就有了。本篇将会介绍动态语言运行时(Dynamic Language Runtime——DLR)如何在C#中工作、DynamicObject and ExpandoObject ,以及 .NET 中 IronPython 的最简单实现。

C# 中的动态语言运行时

.NET 4.0 开始,添加了 DLR (Dynamic Language Runtime) ,并有了动态语言的运行时环境,如 IronPython 和 IronRuby

为了能够理解其本质,我们需要知道静态和动态类型语言之间的不同,静态类型语言中,所有类型及其成员属性和方法的标识发生在编译阶段,但是对于动态语言,系统在执行之前,无法确定关于类型的属性和方法的任何情况。

借助于 DLR 环境,C#可以创建其成员在程序执行阶段被标识的动态对象,并可以与传统的静态类型对象一起使用。

在C#中使用 DLR 是使用动态类型的关键,这允许在编译阶段跳过类型检查。此外,对象声明为动态的,可以在程序操作期间改变其类型。(即 弱类型语言,也就是 C# 的 DLR 是动态弱类型语言环境)

强类型语言在变量类型确定后不允许修改。

下面使用 DLR 的示例:

class Program
{
    static void Main(string[] args)
    {
            dynamic x = 3;     // here x is a integer
            Console.WriteLine(x);
 
            x = "Hello world"; // now x is a string
            Console.WriteLine(x);
 
            x = new Item_mast() 
                { ItemId=1,ItemDesсription="Pen",Cost=10 }; // now x is a Item_mast
            Console.WriteLine(x);
            Console.ReadLine();        
    }
}
public class Item_mast
{
    public int ItemId { get; set; }
    public string ItemDesсription { get; set; }
    public int Cost { get; set; }

    public override string ToString()
    {
        return ItemId.ToString() + ", "+ ItemDesсription + " " + Cost.ToString();
    }
}

输出结果如下:

3
Hello world
1, Pen 10

即使变量x改变了其类型好几次,代码依然运行成功,这也是 dynamicvar 两个关键字的不同。使用 var 关键字的变量声明,其类型在编译时确定,并且在运行时不能更改。

此外,你可能注意到 dynamic 类型 和 object 类型 之间的一些相似,看下面的表达式:

dynamic x = 3;
object x = 3

两个的结果一样。但是,两者是不同的。比如下面:

object obj = 24;
dynamic dyn = 24;
obj += 4; // we can not do it!!! [不能这么操作]
dyn += 4; // now is ok

obj += 4行我们将会看到一个错误,因为 objectint 类型 不能使用 += 操作。使用 dynamic 声明的变量是可以这么操作的,因为其类型只有在运行时才知道。

需要注意的是,dynamic 不仅可以用于变量,也可以用于方法和属性。考虑下面改变后的例子:

public class Item_mast
{
    public int ItemId { get; set; }
    public string ItemDesсription { get; set; }
    public dynamic Cost { get; set; } 

    public dynamic GetPrice(dynamic value, string format)
    {
        if (format == "string")
        {
            return value + " dollar";
        }
        else if (format == "int")
        {
            return value;
        }
        else
        {
            return 0.0;
        }
    }
}

Item_mast类定义了一个动态的Cost属性,因此当为其设置值时,可以是 Item.Cost=10.00Item.Cost="ten",两者都正确。GetPrice是返回动态值的方法,如示例,取决于参数,我们可以返回表示价格的string或数字,方法还接受一个动态变量参数,因此可以传递一个整数或小数数字作为输入值。下面是相关的使用: 【fractional number 小数】

dynamic item1 = new Item_mast() { ItemId = 1, ItemDesсription = "Pen", Cost = 10 };
Console.WriteLine(item1);
Console.WriteLine(item1.GetPrice(10.00, "int"));

dynamic item2 = new Item_mast() 
                { ItemId = 2, ItemDesсription = "Pencil", Cost = "five" };
Console.WriteLine(item2);
Console.WriteLine(item2.GetPrice(5, "string"));
Console.ReadLine();

其运行结果如下:

1, Pen 10
10
2, Pencil five
5 dollar

DynamicObject 和 ExpandoObject

ExpandoObject

C#/.NET开发可以创建,与在JavaScript中使用的非常相似的动态对象,其实现是通过使用Dynamic命名空间,特别是ExpandoObject类提供的。

考虑如下的示例:

dynamic viewbag = new System.Dynamic.ExpandoObject();
viewbag.ItemId = 1;
viewbag.ItemDesсription = "Pen";
viewbag.Cost = 10;

viewbag.Categories = new List<string> { "Flex", "Soft", "Luxury" };

Console.WriteLine($"{viewbag.ItemId} ; 
                    {viewbag.ItemDesсription} ; {viewbag.Cost}");
foreach (var cat in viewbag.Categories)
    Console.WriteLine(cat);

//declare method
viewbag.IncrementCost = (Action<int>)(x => viewbag.Cost += x);
viewbag.IncrementCost(6); // Increase Cost 
Console.WriteLine($"{viewbag.ItemId} ; 
                    {viewbag.ItemDesсription} ; {viewbag.Cost}");

Console.ReadLine();

其结果如下:

1 ; Pen ; 10
Flex
Soft
Luxury
1 ; Pen ; 16

一个动态的ExpandoObject对象可以声明任何表示各种对象的属性,也可以使用委托设置方法。

DynamicObject

DynamicObject类与ExpandoObject非常相似,但是,使用DynamicObject,我们需要通过继承它并实现其方法来创建我们自己的类:

  • TryBinaryOperation(): 两个对象之间执行二进制操作. 等效于标准二进制运算,例如 x + y 的加法。
  • TryConvert(): 执行到特定类型的转换. 等效于C#中的基本转换,例如 (SomeType) obj
  • TryCreateInstance(): 创建一个对象的实例
  • TryDeleteIndex(): 移除索引器
  • TryDeleteMember(): 删除属性或方法
  • TryGetIndex(): 通过索引器获取指定索引的元素. 在C#中,这与 int x = collection[i] 表达式是等效的。
  • TryGetMember(): 获取属性值. 等效于访问属性,例如 string n = item1.ItemDescription
  • TryInvoke(): 作为委托调用一个对象
  • TryInvokeMember(): 方法调用
  • TrySetIndex(): 通过索引器的索引设置元素. 在C#中,这与 collection[i] = x 表达式是等效的;
  • TrySetMember(): 设置属性. 等效于 property.Itemdescription = "Pen" 为 Item 分配值。
  • TryUnaryOperation(): 执行一个类似于 C# x++ 的一元运算。

所有这些方法都有相同的监测模型:均返回一个表示是否操作成功的布尔值,作为第一个参数,他们都使用绑定器或绑定器对象;如果方法表示一个索引器或一个可接受参数的对象方法的调用,则object[]数组用作第二个参数——存储传递到方法或索引器的参数。

除了设置和删除属性和索引器之外,所有操作都返回一个确定值。例如,如果我们获取一个属性值,下面的例子,第三个参数out对象的值被使用,该对象用于存储返回的object

下面是创建 dynamic 对象的示例:

class Item_mast : DynamicObject
{
    Dictionary<string, object> members = new Dictionary<string, object>();

    // set prop
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        members[binder.Name] = value;
        return true;
    }
    // get prop
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = null;
        if (members.ContainsKey(binder.Name))
        {
            result = members[binder.Name];
            return true;
        }
        return false;
    }
    // call method
    public override bool TryInvokeMember
    (InvokeMemberBinder binder, object[] args, out object result)
    {
        dynamic method = members[binder.Name];
        result = method((int)args[0]);
        return result != null;
    }
}

不能直接从 DynamicObject 创建对象,所以 Item_mast 类做为子类使用,里面重定义了三个方法,使用 Dictionary<string, object> 存储所有的类成员,包含属性和方法,其 keys 是属性和方法的名字,values 是属性的值。

使用 TrySetMember 方法设置属性:

bool TrySetMember(SetMemberBinder binder, object value)

此处,binder 参数存储属性名,通过(binder.Name)设置,value 参数是需要设置的值。

TryGetMember 重写方法用于获取属性值。

bool TryGetMember(GetMemberBinder binder, out object result)

同样,binder 包含属性名,result 参数将包含结果值。

TryInvokeMember 用做调用方法

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
    dynamic method = members[binder.Name];
    result = method((int)args[0]);
    return result != null;
}

首先使用 binder 我们获取方法,并传递 转换为 int 类型的 args[0] 参数,设置 result 参数作为方法的结果。也就是说,在此例中,方法将会使用一个 int 类型的参数并返回结果。

在程序中使用这个类作为示例:

static void Main(string[] args)
{
    dynamic item = new Item_mast();
    item.ItemId = 1;
    item.ItemDesсription = "Pen";
    item.Cost = 10;
    Func<int, int> Incr = delegate (int x) { item.Cost += x; return item.Cost; };
    item.IncrementCost = Incr;
    Console.WriteLine($"{item.ItemId} ; {item.ItemDesсription} ; {item.Cost}");         
    item.IncrementCost(6);
    Console.WriteLine($"{item.ItemId} ; {item.ItemDesсription} ; {item.Cost}");
    Console.ReadLine();
}

item.ItemId = 1item.ItemDescription = "Pen" 表达式将会调用 TrySetMember 方法,第一个表达式将会传递一个数字、第二个表达式将传递字符串 "Pen" 作为第二个参数,

返回 item.Cost 将调用 TryGetMember 方法。

同时,item 对象有一个 IncrementCost 方法定义,它表示匿名委托 elegate (int x) { item.Cost += x; return item.Cost; },该委托方法使用数字x,将其相加到 Cost 属性,并返回 item.Cost 新值。当调用该方法时,将会访问 TryInvokeMember 方法,item.Cost 属性的值也将会增加。

使用 DynamicObject 的最大优势是你可以重定义动态类型的行为,例如,在实际中对动态可扩展对象,创建你自己的实现。

在 .NET 中 使用 IronPython

我们似乎会问为什么需要更多的语言,尤其是使用那些另一种的C#语言?DLR环境的一个关键点是对诸如 IronPythonIronRuby 语言的支持,这在编写功能性的客户端脚本时非常有用,甚至可以说当今创建客户端脚本非常普遍,许多程序甚至游戏,都支持不同语言编写的额外的客户端脚本。此外,可能存在Python库的功能在 .NET 中不可用。这些情况下,IronPython 可以帮助我们。

对于使用示例,我们首先需要添加两个NuGet包:DLRIronPython

添加下面最简单的代码,用来使用 Python :

class Program
{
    static void Main(string[] args)
    {
        ScriptEngine engine = Python.CreateEngine();
        engine.Execute("print 'hello, world'");
    }
}

将会看到如下结果:

hello, world

此处的Python表达式print 'hello, world'被使用,并输出一个字符串到控制台。ScriptEngine类表示创建的执行脚本的引擎,Execute() 方法则执行脚本。

此外,我们可以创建一个python文件并执行它。比如 helloword.py 的python文件,并直接将代码添加到文件内容:

engine.ExecuteFile("D://helloword.py");

ScriptScope 对象允许通过接受和安装py脚本,实现交互执行。