65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2022 年 1 月 31 日

CPOL

8分钟阅读

viewsIcon

15565

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 所示

图 1 - 基本 DLR 示例

我们来稍微描述一下代码。即使变量 `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):

图 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 所示

图 3 - ExpandoObject() 用法示例

动态 `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` 成员来存储所有类成员,但我们也将其用于属性和方法。在此字典中,这里的 `keys` 是属性和方法的 `names`,而 `values` 是这些属性的 `values`。
使用 `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)。

图 4 - 添加 DLR 包

接下来,添加 **IronPython**(图 5)

图 5 - 添加 IronPython 包

让我们添加最简单的代码,我们已经在使用了 python:

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

结果是(图 6):

图 6 - 使用 IronPython

在这里,使用了 Python 表达式 `print 'hello, world'`,它将一个字符串输出到控制台。要创建执行脚本的引擎,请使用 `ScriptEngine` 类。它的 `Execute()` 方法执行脚本。
此外,我们可以创建一个文件,例如 `helloword.py` 并将文件的内容直接粘贴到我们的代码中:

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

同样,`ScriptScope` 对象允许你通过接收或安装脚本来与之交互。然而,这已经超出了本文的范围。

结论

最后,我们研究了动态语言运行时 (DLR) 在 C# 中的工作原理,如何使用 `DynamicObject` 和 `ExpandoObject`,以及 IronPython 在 .NET 中的最简单示例。

历史

  • 2022 年 1 月 31 日:初始版本
© . All rights reserved.