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





5.00/5 (8投票s)
C# 中动态语言运行时 DLR 的概述
引言
尽管 C# 属于静态类型语言,但在近几个版本中也添加了一些动态特性。在本文中,我想展示 **动态语言运行时 (DLR)** 在 C# 中的工作原理,`DynamicObject` 和 `ExpandoObject` 的用法,以及 **IronPython** 在 .NET 中的最简单用法。
C# 中的动态语言运行时 DLR
**DLR (动态语言运行时)** 自 **.NET 4.0** 起被添加,它代表了 **IronPython** 和 **IronRuby** 等动态语言的运行时环境。
要理解这项创新的本质,你需要了解静态类型语言和动态类型语言的区别。在静态类型语言中,所有类型及其成员(属性和方法)的识别发生在编译阶段,而在动态语言中,系统直到执行时才知道类型的属性和方法。
得益于这种 **DLR** 环境,C# 可以创建动态对象,其成员在程序执行阶段被识别,并与传统的静态类型对象一起使用。
动态类型的用法是 **DLR** 在 C# 中使用的关键点,因此允许你在编译阶段跳过类型检查。此外,声明为 dynamic 的对象可以在程序运行期间更改其类型。例如:
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();
}
}
结果如图 1 所示
我们来稍微描述一下代码。即使变量 `x` 更改了它的类型几次,这段代码也能正常工作。这是 `dynamic` 和 `var` 之间的关键区别。对于使用 `var` 关键字声明的变量,类型在编译时确定,然后在运行时不会改变。此外,你可能会注意到 `dynamic` 类型和 `object` 类型之间的一些相似之处。我们可以轻松地替换表达式:
dynamic x = 3;
to
object x = 3
我们得到了相同的结果。
然而,与 `object` 类型也存在差异。例如:
object obj = 24;
dynamic dyn = 24;
obj += 4; // we can not do it!!!
dyn += 4; // now is ok
在 `obj += 4` 这一行,我们会看到一个错误,因为 `+=` 操作不能应用于 `object` 和 `int` 类型。对于声明为 `dynamic` 的变量,这是可能的,因为它的类型只会在运行时才知道。
需要注意的是,`dynamic` 不仅可以应用于变量,还可以应用于方法和属性。让我们修改 `class` 并考虑下一个示例:
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_mass` 类定义了一个动态 `Cost` 属性,所以当我们设置这个属性的值时,我们可以写 `Item.Cost=10.00`,也可以写 `Item.Cost="ten"`。这两种方式都是正确的。还有一个 `GetPrice` 方法,它返回一个动态值。例如,根据参数,我们可以返回价格的字符串表示形式或数值表示形式。该方法还将动态类型作为参数。因此,我们可以将整数或分数作为输入值传递。让我们看看具体应用:
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();
结果是(图 2):
在这一部分,我们通过示例研究了动态类型的用法。
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();
结果如图 3 所示
动态 `ExpandoObject` 对象可以声明任何属性,这些属性可以代表各种对象。你还可以使用委托设置方法。
动态对象
`DynamicObject` 类与 `ExpandoObject` 非常相似。但是,对于 `DynamicObject`,我们需要通过继承 `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()`:设置属性。相当于将项的值赋给属性,例如 `Itemdescription = "Pen"`。
- `TryUnaryOperation()`:执行一元运算,类似于 C# 中的一元运算:`x++`。
这些方法中的每一种都具有相同的检测模型:它们都返回一个布尔值,指示操作是否成功。作为第一个参数,它们都接受一个绑定器或绑定器对象。如果方法代表对索引器或可以接受参数的对象方法的调用,则使用 `object[]` 数组作为第二个参数——它存储传递给方法或索引器的参数。
几乎所有操作(除了设置和删除属性和索引器)都会返回一个特定值(例如,如果我们获取属性的值。在这种情况下,使用第三个 `out` 参数 `object value`,它用于存储返回的 `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
使用 `TrySetMember()` 方法,我们设置属性:
bool TrySetMember(SetMemberBinder binder, object value)
在这里,绑定器参数存储要设置的属性的名称(`binder.Name`),值是要设置的值。
`TryGetMember` 是一个被重写的方法,我们用它来获取属性值。
bool TryGetMember(GetMemberBinder binder, out object result)
同样,绑定器包含属性的名称,而 result 参数将包含结果的值。
The TryInvokeMember method is defined for calling methods:
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
dynamic method = members[binder.Name];
result = method((int)args[0]);
return result != null;
}
首先,我们使用绑定器获取方法,然后将其传递 `args[0]` 参数,先将其强制转换为 `int` 类型,并将方法的结果设置在 `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 = 1` 和 `item.ItemDescription = "Pen"` 将调用 `TrySetMember` 方法,该方法在第一种情况下将数字作为第二个参数传递,在第二种情况下将字符串 `"Pen"` 传递。
返回 `item.Cost` 调用 `TryGetMember` 方法。
此外,`item` 对象还有一个定义的 `IncrementCost` 方法,它代表一个匿名委托 `delegate (int x) { item.Cost+=x; return item.Cost; }` 的操作。委托接受数字 `x`,将其增加 `Cost` 属性,并返回新值 `item.Cost`。并且在调用此方法时,将访问 `TryInvokeMember` 方法。因此,`item.Cost` 属性的值将被递增。
优点是你可以重新定义动态对象在操作时的行为,即实际实现你自己的可动态扩展对象。
在这一部分,我们通过示例研究了 `DynamicObject` 和 `ExpandoObject` 的用法。
在 .NET 中使用 IronPython。
似乎有疑问,为什么我们需要更多的语言,尤其是那些在 C# 语言内部使用的语言?然而,**DLR** 环境的关键点之一是支持 **IronPython** 和 **IronRuby** 等语言。这在编写功能性客户端脚本时可能很有用。甚至可以说,如今客户端脚本的创建非常普遍,许多程序甚至游戏都支持添加用各种语言编写的客户端脚本。此外,可能存在 **Python** 库,其功能在 .NET 中不可用。在这种情况下,IronPython 同样可以帮助我们。
让我们看一个例子。首先,我们需要添加所有必需的 `NuGet` 包。为此,我将使用一个批处理管理器。首先,让我们添加 **DLR** 包(图 4)。
接下来,添加 **IronPython**(图 5)
让我们添加最简单的代码,我们已经在使用了 python:
class Program
{
static void Main(string[] args)
{
ScriptEngine engine = Python.CreateEngine();
engine.Execute("print 'hello, world'");
}
}
结果是(图 6):
在这里,使用了 Python 表达式 `print 'hello, world'`,它将一个字符串输出到控制台。要创建执行脚本的引擎,请使用 `ScriptEngine` 类。它的 `Execute()` 方法执行脚本。
此外,我们可以创建一个文件,例如 `helloword.py` 并将文件的内容直接粘贴到我们的代码中:
engine.ExecuteFile("D://helloword.py");
同样,`ScriptScope` 对象允许你通过接收或安装脚本来与之交互。然而,这已经超出了本文的范围。
结论
最后,我们研究了动态语言运行时 (DLR) 在 C# 中的工作原理,如何使用 `DynamicObject` 和 `ExpandoObject`,以及 IronPython 在 .NET 中的最简单示例。
历史
- 2022 年 1 月 31 日:初始版本