目录
更改可见性:使用 MoonSharpHidden 和 MoonSharpVisible
6.Sharing objects(共享对象)
让 Lua 和 C# 互相交流。
文档地址:MoonSharp
注意:本页面列出的一些功能反映了主分支的当前状态(因此,它们可能是最新版本中缺失的功能)。
MoonSharp 的一个便捷功能是能够与脚本共享 .NET 对象。
默认情况下,类型会将其所有公共方法、公共属性、公共事件和公共字段与 Lua 脚本共享。可以使用 MoonSharpVisible
属性来覆盖此默认可见性。
建议使用专用对象作为 CLR 代码和脚本代码之间的接口(而不是将应用程序的内部模型暴露给脚本)。许多设计模式(如适配器、外观、代理等)可以帮助设计这样的接口层。这一点尤其重要,原因如下:
-
限制脚本可以做什么和不能做什么(安全性!你希望模组作者找到删除最终用户个人文件的方法吗?)
-
提供一个对脚本作者有意义的接口
-
单独记录接口
-
允许内部逻辑和模型在不破坏脚本的情况下进行更改
出于这些原因,MoonSharp 默认要求显式注册可供脚本使用的类型。
如果你处于可以信任脚本的场景中,可以通过 UserData.RegistrationPolicy = InteropRegistrationPolicy.Automatic;
全局启用自动注册。这很危险,你已经被警告过了。
那么,让我们看看菜单上有什么:
-
首先谈谈类型描述符 - 一些理论,解释幕后的工作原理以及如何覆盖整个互操作系统
-
保持简单 - 最简单的入门方式
-
稍微复杂一点 - 深入探讨,增加一些复杂性和细节
-
调用静态成员 - 如何调用静态成员
-
应该使用 ':' 还是 '.' ? - 关于如何调用方法的简单问题
-
重载 - 如何处理重载
-
ByRef 参数(C# 中的 ref/out) - 如何处理 ref/out 参数
-
索引器 - 如何处理索引器
-
用户数据上的运算符和元方法 - 如何重载运算符等
-
扩展方法 - 如何使用扩展方法
-
事件 - 如何使用事件
-
互操作访问模式 - 什么是互操作访问模式及其工作原理
-
使用 MoonSharpHidden 和 MoonSharpVisible 更改可见性 - 如何覆盖成员的可见性
-
移除成员 - 如何移除成员的可见性
内容很多,让我们开始吧。
首先谈谈类型描述符
首先是一些关于互操作如何实现的小理论。每个 CLR 类型都被包装到一个“类型描述符”中,该描述符的作用是向脚本描述 CLR 类型。注册一个类型进行互操作意味着将该类型与一个描述符(MoonSharp 可以自己创建)关联起来,该描述符将用于分发方法、属性等。
从下一节开始,我们将讨论 MoonSharp 提供的“自动”描述符,但你可以实现自己的描述符以提高速度、增加功能、安全性等。
如果你想实现自己的描述符(这并不容易,除非有必要,否则不应这样做),你可以遵循以下路径:
-
创建一个专门的
IUserDataDescriptor
来描述你自己的类型 - 这是最困难的方式 -
让你的类型实现
IUserDataType
接口。这更容易,但意味着你无法在没有对象实例的情况下处理静态成员。 -
扩展或嵌入
StandardUserDataDescriptor
并更改你需要的方面,同时保持其余行为。
为了帮助创建描述符,以下类可用:
-
StandardUserDataDescriptor
- 这是 MoonSharp 实现的类型描述符 -
StandardUserDataMethodDescriptor
- 这是单个方法/函数的描述符 -
StandardUserDataOverloadedMethodDescriptor
- 这是重载和/或扩展方法的描述符 -
StandardUserDataPropertyDescriptor
- 这是单个属性的描述符 -
StandardUserDataFieldDescriptor
- 这是单个字段的描述符
关于与值类型作为用户数据的互操作的一点说明。
就像调用函数时传递值类型作为参数一样,脚本将在用户数据的副本上操作,因此,例如,更改用户数据中的字段不会反映在原始值上。同样,这与值类型的标准行为没有什么不同,但这足以让人感到惊讶。
此外,值类型不像引用类型那样支持所有优化,因此某些操作在值类型上可能比在引用类型上慢。
保持简单
好的,让我们来看第一个例子。
[MoonSharpUserData]
class MyClass
{
public double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
double CallMyClass1()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Automatically register all MoonSharpUserData types
UserData.RegisterAssembly();
Script script = new Script();
// Pass an instance of MyClass to the script in a global
script.Globals["obj"] = new MyClass();
DynValue res = script.DoString(scriptCode);
return res.Number;
}
在这里我们:
-
定义了一个带有
[MoonSharpUserData]
属性的类。 -
将一个
MyClass
对象的实例作为全局变量传递到脚本中。 -
从脚本中调用了
MyClass
的一个方法。所有回调的映射规则都适用。
稍微复杂一点
让我们尝试一个更复杂的例子。
class MyClass
{
public double CalcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
static double CallMyClass2()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Register just MyClass, explicitely.
UserData.RegisterType<MyClass>();
Script script = new Script();
// create a userdata, again, explicitely.
DynValue obj = UserData.Create(new MyClass());
script.Globals.Set("obj", obj);
DynValue res = script.DoString(scriptCode);
return res.Number;
}
这里的主要区别是:
-
不再需要
[MoonSharpUserData]
属性。我们不再需要它了。 -
使用
RegisterType
而不是RegisterAssembly
来注册特定类型。 -
我们显式地创建了用户数据
DynValue
。
另外,请注意,C# 代码中的方法名是 CalcHypotenuse
,但在 Lua 脚本中调用时使用的是 calcHypotenuse
。
只要其他版本不存在,MoonSharp 会自动以某些有限的方式调整大小写以匹配成员,以便更好地适应不同语言的语法约定。例如,一个名为 SomeMethodWithLongName
的成员在 Lua 脚本中也可以通过 someMethodWithLongName
或 some_method_with_long_name
来访问。
调用静态成员
假设我们的类中有一个静态方法 calcHypotenuse。
[MoonSharpUserData]
class MyClassStatic
{
public static double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
我们可以通过两种方式调用它。
第一种方式 - 静态方法可以通过实例透明地调用,无需额外操作,一切都是自动完成的。
double MyClassStaticThroughInstance()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Automatically register all MoonSharpUserData types
UserData.RegisterAssembly();
Script script = new Script();
script.Globals["obj"] = new MyClassStatic();
DynValue res = script.DoString(scriptCode);
return res.Number;
}
另一种方式 - 可以通过直接传递类型(或使用 UserData.CreateStatic
方法)创建一个占位符用户数据(placeholder userdata):
double MyClassStaticThroughPlaceholder()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Automatically register all MoonSharpUserData types
UserData.RegisterAssembly();
Script script = new Script();
script.Globals["obj"] = typeof(MyClassStatic);
DynValue res = script.DoString(scriptCode);
return res.Number;
}
应该使用 “:” 还是 “.”
考虑到上述例子中的代码,一个很好的问题是,是否应该使用这种语法。
return obj.calcHypotenuse(3, 4);
或者:
return obj:calcHypotenuse(3, 4);
99.999% 的情况下,这不会产生影响。 MoonSharp 知道调用是在用户数据上进行的,并会相应地处理。
但在某些极端情况下,可能会有所不同 —— 例如,如果一个属性返回一个委托,并且你打算立即调用该委托,同时将原始对象作为实例传递。这种情况非常罕见,如果真的遇到这种情况,你需要手动处理。
重载
支持重载方法。重载方法的调度有些神秘,并不像 C# 中的重载调度那样具有确定性。这是由于存在一些歧义性。例如,一个对象可以声明以下两个方法:
void DoSomething(int i) { ... }
void DoSomething(float f) { ... }
鉴于 Lua 中的所有数字都是双精度浮点数(double),MoonSharp 如何知道该调用哪个方法呢?
为了解决这个问题,MoonSharp 会根据输入类型为所有重载方法计算一个启发式因子,并选择最佳的重载方法。如果你认为 MoonSharp 以错误的方式解析了重载方法,请到论坛或 Discord 上反馈,以便校准启发式规则。
MoonSharp 会尽可能地保持启发式权重的稳定性,如果多个方法的得分相同,它会始终确定性地选择同一个方法(以在不同构建和平台之间提供一致的体验)。
尽管如此,MoonSharp 仍然有可能选择一个与你预期不同的重载方法。因此,确保重载方法执行等效的操作非常重要,这样可以最大限度地减少调用错误重载方法的影响。这本身就是一个最佳实践,但在这里值得再次强调这一概念。
ByRef 参数(C# 中的 ref/out)
MoonSharp 能够正确地将 ByRef
方法参数作为多个返回值进行封送处理。不过,这种支持并非没有副作用,因为带有 ByRef
参数的方法无法被优化。
假设我们有这样一个 C# 方法(为了说明问题,假设它被暴露在一个名为 myobj
的用户数据中):
public string ManipulateString(string input, ref string tobeconcat, out string lowercase)
{
tobeconcat = input + tobeconcat;
lowercase = input.ToLower();
return input.ToUpper();
}
我们可以通过以下方式从 Lua 代码中调用该方法(并获取结果):
x, y, z = myobj:manipulateString('CiAo', 'hello');
-- x will be "CIAO"
-- y will be "CiAohello"
-- z will be "ciao"
尽管支持 ByRef
参数,但它们会导致方法始终通过反射调用,因此可能会在非 AOT 平台上降低性能(AOT 平台本身已经很慢了……请将你的抱怨发送给苹果,而不是我)。
索引器
C# 允许创建索引器方法。例如:
class IndexerTestClass
{
Dictionary<int, int> mymap = new Dictionary<int, int>();
public int this[int idx]
{
get { return mymap[idx]; }
set { mymap[idx] = value; }
}
public int this[int idx1, int idx2, int idx3]
{
get { int idx = (idx1 + idx2) * idx3; return mymap[idx]; }
set { int idx = (idx1 + idx2) * idx3; mymap[idx] = value; }
}
}
作为 Lua 语言的扩展,MoonSharp 允许在方括号内使用表达式列表来索引用户数据(userdata)。例如,假设 o
是上述类的一个实例,以下代码是有效的:
-- sets the value of an indexer
o[5] = 19;
-- use the value of an indexer
x = 5 + o[5];
-- sets the value of an indexer using multiple indices (not standard Lua!)
o[1,2,3] = 19;
-- use the value of an indexer using multiple indices (not standard Lua!)
x = 5 + o[1,2,3];
需要注意的是,在非用户数据(userdata)上使用多个索引会引发错误。这包括通过元方法(metamethods)的场景,但如果元表的 __index
字段设置为用户数据(包括递归的情况),则支持多索引。
简而言之,以下代码是有效的:
m = {
__index = o,
__newindex = o
}
t = { }
setmetatable(t, m);
t[10,11,12] = 1234; return t[10,11,12];";
并且这不会:
m = {
-- we can't even write meaningful functions here, but let's pretend...
__index = function(obj, idx) return o[idx] end,
__newindex = function(obj, idx, val) end
}
t = { }
setmetatable(t, m);
t[10,11,12] = 1234; return t[10,11,12];";
userdata 上的运算符和元方法
支持重载运算符。
以下是标准描述符(descriptor)如何调度运算符的说明,但你可以在单元测试代码中查看实际示例。
显式元方法实现
首先,如果一个或多个带有 MoonSharpUserDataMetamethod
装饰的静态方法被实现,这些方法将用于调度相应的元方法。请注意,如果这些方法存在,它们将优先于以下任何其他条件。
__pow
、__concat
、__call
、__pairs
和 __ipairs
只能通过这种方式实现(除非使用自定义描述符)。
例如,以下代码将实现连接(..
)运算符:
[MoonSharpUserDataMetamethod("__concat")]
public static int Concat(ArithmOperatorsTestClass o, int v)
{
return o.Value + v;
}
[MoonSharpUserDataMetamethod("__concat")]
public static int Concat(int v, ArithmOperatorsTestClass o)
{
return o.Value + v;
}
[MoonSharpUserDataMetamethod("__concat")]
public static int Concat(ArithmOperatorsTestClass o1, ArithmOperatorsTestClass o2)
{
return o1.Value + o2.Value;
}
算术运算符的隐式元方法实现
如果找到运算符重载,算术运算符将自动处理。
public static int operator +(ArithmOperatorsTestClass o, int v)
{
return o.Value + v;
}
public static int operator +(int v, ArithmOperatorsTestClass o)
{
return o.Value + v;
}
public static int operator +(ArithmOperatorsTestClass o1, ArithmOperatorsTestClass o2)
{
return o1.Value + o2.Value;
}
在 Lua 脚本中,可以在数字和该对象之间使用 +
运算符。
加法、减法、乘法、除法、取模和一元取反运算符都支持这种方式。
比较运算符、长度运算符和 __iterator
元方法
相等运算符(==
和 ~=
)会自动通过 System.Object.Equals
来解析。
比较运算符(<
、>=
等)会自动通过 IComparable.CompareTo
来解析,前提是该对象实现了 IComparable
。
长度运算符(#
)会分派到 Length
或 Count
属性,前提是该对象实现了这些属性。
最后,__iterator
元方法会自动分派到 GetEnumerator
,前提是该类实现了 System.Collections.IEnumerable
。
扩展方法
扩展方法支持
扩展方法必须通过 UserData.RegisterExtensionType
或 RegisterAssembly(<assembly>, true)
进行注册。前者用于注册包含扩展方法的单个类型,后者则会注册指定程序集中包含的所有扩展类型。
扩展方法会与其他方法的重载一起被解析。
事件
事件支持
事件也受到支持,但以一种较为简化的方式实现。只有符合以下约束条件的事件才会被支持:
-
事件必须在引用类型中声明。
-
事件必须同时实现
add
和remove
方法。 -
事件处理程序的返回类型必须是
System.Void
(在 VB.NET 中,必须是Sub
)。 -
事件处理程序的参数数量不得超过 16 个。
-
事件处理程序的参数不能是值类型或按引用传递的参数。
-
事件处理程序的签名不能包含指针或未解析的泛型。
-
事件处理程序的所有参数必须可以转换为 MoonSharp 类型。
这些约束的存在是为了尽可能避免在运行时生成代码。
尽管这些约束看起来可能有些限制,但实际上它们反映了事件设计中的一些最佳实践。它们足以支持 EventHandler
和 EventHandler<T>
类型的事件处理程序,而这些类型是目前最常见的事件处理程序(前提是至少将 EventArgs
注册为用户数据)。
以下是一个使用事件的简单示例:
class MyClass
{
public event EventHandler SomethingHappened;
public void RaiseTheEvent()
{
if (SomethingHappened != null)
SomethingHappened(this, EventArgs.Empty);
}
}
static void Events()
{
string scriptCode = @"
function handler(o, a)
print('handled!', o, a);
end
myobj.somethingHappened.add(handler);
myobj.raiseTheEvent();
myobj.somethingHappened.remove(handler);
myobj.raiseTheEvent();
";
UserData.RegisterType<EventArgs>();
UserData.RegisterType<MyClass>();
Script script = new Script();
script.Globals["myobj"] = new MyClass();
script.DoString(scriptCode);
}
请注意,这次事件是由 Lua 代码触发的,但它同样可以由 C# 触发,且不会出现任何问题。
添加和移除事件处理程序是较慢的操作,因为它们是在线程锁下通过反射执行的。另一方面,处理事件本身并不会带来显著的性能损失。
关于 InteropAccessMode 的说明
如果你在集成开发环境(IDE)中输入了到目前为止的所有示例,你可能会注意到大多数方法都有一个可选的参数,类型为 InteropAccessMode
。
InteropAccessMode
定义了标准描述符如何处理对 CLR(公共语言运行时)事物的回调。以下是可用的值:
-
Reflection
:使用反射来调用 CLR 方法或访问 CLR 属性。这种方式较为通用,但性能较低。 -
Preoptimized
:在第一次调用时对方法进行预优化,以提高后续调用的性能。 -
LazyOptimized
:在第一次调用时延迟优化方法,适用于需要平衡性能和初始化开销的场景。 -
BackgroundOptimized
:在后台线程中对方法进行优化,以避免阻塞主线程。
有一个静态属性 UserData.DefaultAccessMode
,用于指定默认的访问模式(当前默认值为 LazyOptimized
,除非被更改)。
Reflection | Optimization is not performed and reflection is used everytime to access members. This is the slowest approach but saves a lot of memory if members are seldomly used. 翻译:没有进行优化,每次访问成员时都使用反射。这是最慢的方法,但如果很少使用成员,则可以节省大量内存。 |
---|---|
LazyOptimized | This is a hint, and MoonSharp is free to "downgrade" this to Reflection . Optimization is done on the fly the first time a member is accessed. This saves memory for all members that are never accessed, at the cost of an increased script execution time.翻译:这是一个提示,MoonSharp 可以自由地将其“降级”到反射。第一次访问成员时进行即时优化。这样可以为从未访问过的所有成员节省内存,但代价是增加了脚本执行时间。 |
Preoptimized | This is a hint, and MoonSharp is free to "downgrade" this to Reflection . Optimization is done in a background thread which starts at registration time. If a member is accessed before optimization is completed, reflection is used.翻译:这是一个提示,MoonSharp 可以自由地将其“降级”到反射。优化在注册时开始的后台线程中执行。如果在优化完成之前访问了成员,则使用反射。 |
BackgroundOptimized | This is a hint, and MoonSharp is free to "downgrade" this to Reflection . Optimization is done at registration time.翻译:这是一个提示,MoonSharp可以自由地将其"降级"到反射。优化是在注册时完成的。 |
HideMembers | Members are simply not accessible at all. Can be useful if you need a userdata type whose members are hidden from scripts but can still be passed around to other functions. See also AnonWrapper and AnonWrapper<T> .翻译:成员完全不可访问。如果您需要一个用户数据类型,其成员对脚本是隐藏的,但仍然可以传递给其他函数,这可能很有用。另请参阅AnonWrapper和AnonWrapper<T>。 |
Default | Use the default access mode 翻译:使用默认的访问模式。 |
请注意,许多模式 - 特别是 LazyOptimized、Preoptimized 和 BackgroundOptimized - 只是"提示",MoonSharp 可以自由地将它们降级为 Reflection。例如,在代码提前编译的平台(如iPhone和iPad)上就会发生这种情况。
更改可见性:使用 MoonSharpHidden 和 MoonSharpVisible
可以使用 MoonSharpHidden
和/或 MoonSharpVisible
属性来覆盖成员的默认可见性(MoonSharpHidden
是 MoonSharpVisible(false)
的简写)。以下是一些带有注释的示例——非常简单:
public class SampleClass
{
// Not visible - it's private
private void Method1() { }
// Visible - it's public
public void Method2() { }
// Visible - it's private but forced visible by attribute
[MoonSharpVisible(true)]
private void Method3() { }
// Not visible - it's public but forced hidden by attribute
[MoonSharpVisible(false)]
public void Method4() { }
// Not visible - it's public but forced hidden by attribute
[MoonSharpHidden]
public void Method4() { }
// Not visible - it's private
private int Field1 = 0;
// Visible - it's public
public int Field2 = 0;
// Visible - it's private but forced visible by attribute
[MoonSharpVisible(true)]
private int Field3 = 0;
// Not visible - it's public but forced hidden by attribute
[MoonSharpVisible(false)]
public int Field4 = 0;
// Not visible at all - it's private
private int Property1 { get; set; }
// Read/write - it's public
public int Property2 { get; set; }
// Readonly - it's public, but the setter is private
public int Property3 { get; private set; }
// Write only! - the MoonSharpVisible makes the getter hidden and the setter visible!
public int Property4 { [MoonSharpVisible(false)] get; [MoonSharpVisible(true)] private set; }
// Write only! - the MoonSharpVisible makes the whole property hidden but another attribute resets the setter as visible!
[MoonSharpVisible(false)]
public int Property5 { get; [MoonSharpVisible(true)] private set; }
// Not visible at all - the MoonSharpVisible hides everything
[MoonSharpVisible(false)]
public int Property6 { get; set; }
// Not visible - it's private
private event EventHandler Event1;
// Visible - it's public
public event EventHandler Event2;
// Visible - it's private but forced visible by attribute
[MoonSharpVisible(true)]
private event EventHandler Event3;
// Not visible - it's public but forced hidden by attribute
[MoonSharpVisible(false)]
public event EventHandler Event4;
// Not visible - visibility modifiers over add and remove are not currently supported!
[MoonSharpVisible(false)]
public event EventHandler Event5 { [MoonSharpVisible(true)] add { } [MoonSharpVisible(true)] remove { } }
}
移除成员
有时需要从已注册的类型中移除某些成员,以对脚本隐藏它们。有几种方法可以实现这一点。一种是在类型注册后手动移除这些成员:
var descr = ((StandardUserDataDescriptor)(UserData.RegisterType<SomeType>()));
descr.RemoveMember("SomeMember");
或者,只需将此属性添加到类型声明中:
[MoonSharpHide("SomeMember")]
public class SomeType
...
这是非常重要的,因为你可能希望隐藏某些你没有重写的继承成员。
end