Forwarding .NET Libraries / 转发 .NET 库
通过使用 .NET 库,我们能直接拿到浩瀚的节点来打草图。不过其中很多库放到 VL 的数据流环境里并不好用。
为了让这些库对更随性的用户也能用得上,我们常常想精确地筛选:原始库里究竟哪些节点和类型该被他们看到。转发让我们能插入极薄的一层包装,方便地做这种筛选。
转发的理由
- 有选择地转发 .NET .dll 里的类型和运算器
- 调整类型上与 VL 相关的元信息(比如可变性和已知类型结构)
- 为 VL 里的节点和类型挑一个合适的目录
- 做简单的单位或类型转换(比如把弧度制的角度换成周期制)
- 重命名针脚、运算器、类型
- 给输入针脚设定默认值
- 提供便利的过程节点,把一些底层功能包装成更高层的节点
- 设计可销毁对象的生命周期管理
从一个库里转发类型时有一点很重要:我们不希望引入新的包装类型。因此使用转发不会引入新类型!
被转发的类型与原始库是兼容的 —— 这样用户就能回退到原始库的底层功能,并把它与高层包装的用法混在一起用。
另外,这层包装可以充当一道有用的屏障,把 vl 节点库的最终用户与原始库的变动隔开。原始库改了名字之类的时候,与其让 vl 用户直面这些变动,转发让我们有办法在这种情况下不把用户的草图弄坏。
转发类型
典型的做法是,你创建一个 .vl 文档,用来转发一个或多个 .NET .dll 或 C# 项目(.csproj)里的类型。这样,你这个节点库的用户唯一需要引用的东西就是这一个 .vl 文档。
创建类型转发
1. 设一个指向 .NET .dll 或 .csproj 的引用
在一个空白的 .vl 文档里,设好指向你想转发类型的那些 .NET .dll 或 .csproj 文件的引用。见引用文件(英文)。
2. 准备好一个目录
被导入的类型会出现在你把它放进的那个目录里。所以,先在你文档的定义草图里把需要的目录建好。
3. 创建类型转发
创建类型转发有两种方式:
3.1 从方案浏览器拖放
- 打开你想把类型放进去的那个 group
- 打开方案浏览器
- 选 “.NET Dependencies”
- 找到你想转发的类型
- 把这个类型拖放进那个 group
把类型从方案浏览器拖放进 group
3.2 手动
- 打开你想把类型放进去的那个 group
- 新建一个 Process 草图
- 把
patch type设为 “Forward” - 在这个草图上设一个
type-annotation
左:点开 patch type 下拉,设为 “Forward”。右:点 type-annotation,从节点浏览器里选一个类型
配置类型转发
重命名类型
通常你会想保留原始库里类型的名字。如果你有充分的理由改名,那就改。
重命名一个类型
转发所有节点
创建类型转发时,这个类型的每一个运算器默认都会被当作节点转发出去。如果你更愿意只有选择地转发其中一部分运算器,就取消勾选 “Forward All Nodes” 这个选项。
Forward All Nodes
即使这个选项开着,你仍然可以为个别运算器单独创建运算器转发,从而调整它们的转发方式,见下文。
可变性
.NET 库并不带「这个类型可不可变」这样的元信息。因此我们需要手动告诉 VL —— 相应地设好 mutable 标记。
Mutable 复选框
由于大多数 .NET 类型是可变的,这个标记默认是开着的。下面是判断一个 .NET 类型是否不可变的方法:
- 它只有只读字段
- 它的每一个字段都是不可变类型
- 可选:它有
WithFoo(TFoo newValue)这样的方法,用来取得该类型的一个新实例(也就是一份新的不可变快照)—— 新实例的所有字段都取当前实例的值,只有Foo这个字段被设为newValue
C# 接下来的版本里请留意 record,它应该能减轻写不可变类型的痛苦。
已知类型结构
(上游此处待写)
(上游此处待补图:已知类型结构)
创建默认值
成员运算器节点常常期望主输入上有一个该类型的值,只要那儿什么都没连,它就抛「空指针异常」。为了避免这一点,我们需要告诉 VL:需要时它该怎么构造这个类型的一个默认实例。
做法很简单:在类型转发草图里创建一个叫 CreateDefault 的运算器,把它实现成返回该类型的一个实例。这往往只需要返回该类型某个构造函数的结果,别无其他。
为一个类型创建默认值
过程节点
每一个类型转发也可以直接暴露出一个过程节点。这与从普通草图里暴露一个过程节点完全一样。
这样你就得到了一个可用的、属于你 C# 类型的过程节点。
如果你想从同一个类型转发里暴露多于一个过程节点,那么每多一个过程节点,你就得另建一个过程定义。这些定义不转发类型,只是用该类型的运算器来搭出想要的过程。
转发运算器
如上所示,一个类型转发可以轻松地自动转发它的全部运算器。不过即便 “Forward All Nodes” 开着,手动转发某些运算器以便调整它们的针脚,仍然是讲得通的。
为个别运算器创建转发:
- 打开你想把这个运算器放进去的那个类型
- 打开方案浏览器
- 选 “.NET Dependencies”
- 找到你想导入的运算器
- 把这个运算器拖放进那个类型
把运算器放进类型里
你也可以选中多个运算器,一次性把它们放进草图。
现在你有了一个转发运算器定义,包在你想转发的那个节点外面。被转发节点的所有针脚都会自动反映在这个转发定义的签名里。这也意味着:该节点签名的任何变动(也就是它底层的 .NET 代码里增加/改名/删除了针脚)都会自动反映到转发定义的签名上。如果因为某些原因你不想要这种行为,见下文「手动管理签名」。
即便不手动管理签名,你仍然可以对一个转发做下面这些改动:
重命名针脚
如果你有充分的理由改一个针脚的名字,比如为了让它符合 VL 命名约定,那就手动为这个针脚建一个输入或输出,然后改名。
重命名一个针脚
设定默认值
运算器的参数很少带有意义的默认值。要转发一个带合适默认值的针脚,就手动为这个针脚建一个输入,并给它设默认值。
通过中键点击或 Rightclick > Configure 给输入设默认值
隐藏针脚
即使「自动转发所有针脚」开着,你也可以覆盖个别针脚的转发 —— 只要往它上面连一个 IOBox。
隐藏一个针脚
类型或单位转换
转发是做简单类型或单位转换的好地方。设想一个运算器接受弧度制的角度,而你想用符合 vl 习惯的周期制。
SineWave 接受周期制的角度
显示目录
默认情况下成员运算器开着这一项,静态运算器不开。要改这个默认值,唯一说得过去的理由是像 Vector (Join) 这样的节点 —— 它们是成员这一事实,对草图的可读性并不重要。对比下面两者:
Vector (Join) [2D.Vector2] 不显示它的目录,而 GetSlice [Collections.Spreads] 显示
在你正在转发的那个运算器的标题栏上右键,选 Configure > Show Category,来指定这个节点是否显示它的类型目录。
Show Category 复选框
手动管理签名
转发一个节点时,你通常希望它的签名自动与外层定义的签名同步。这就是为什么管理这一行为的两个选项默认都是开着的:
- Locked Signature(也就是由系统管理,而非用户手动管理)
- Connect to Signature(只在签名被锁定时起作用)
关掉它们的理由可能是:你想为自己的 vl 节点库建立一套稳定的 API,不希望它自动跟着底层 .NET 库的变动走。既然 .NET 库的一处改动可能对你 vl 节点库的用户造成不兼容,你就会希望有机会先审阅这些变动,再决定怎样把它们转发到你的 API 上。
“Locked Signature” 和 “Connect to Signature” 这两个功能并不限于在转发定义里使用。别的场景下它们也可能有用。
Locked Signature
取消勾选 “Locked Signature” 有两个后果:
- 签名里的针脚不再按它们在草图里的横向位置自动排序
- 对那些开了 “Connect to Signature” 的节点,若其签名变了,针脚不会再被自动加进/移出签名。取而代之,签名上会显示警告,让你去查看这些变动并作出反应
另见运算器签名。
Connect to Signature
对从方案浏览器里拖进来准备转发的节点,Connect to Signature 默认是开着的。它能省下几次点击 —— 自动把这个节点连到外层签名上,就好像你为每个针脚都建了一个同名针脚并连上一样。如果你想更手动地控制哪些针脚被转发,可以把这个功能关掉。
在你正在转发的那个节点上右键,选 Configure > Connect to Signature。
“Connect to Signature” 功能
转发枚举
要把一个 .dll 里的枚举转发给 .vl 文档的使用者,只要把这个枚举拖放到草图上。
枚举转发
包装非标准的事件或 Delegate
第三方库里的事件或 Delegate,常常正是写一小段 C# 包装的理由。符合 .NET Core 事件模式(英文)的事件会被方便地自动转换成 vl 里的 Observable,但很多库用的是非标准的事件或 Delegate —— 这种情况下你得用 System.Reactive 这个 NuGet 提供的 Observable.FromEvent(英文),在 C# 里手写一个到 Observable 的转换。
举个例子。假设这个库有个数据类型 Tablet,上面定义了这样一个事件:
public event PacketArrivalEventHandler (int x, int y, int z);
而你想在这个事件被触发时,通过 VL 里某个节点的输出收到通知。
首先你得为「你想在 VL 里收到的通知类型」建一个类。它大概长这样:
public class PackageArgs: EventArgs
{
public readonly int X;
public readonly int Y;
public readonly int Z;
public PackageArgs(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
}
然后你可以造一个静态运算器节点,它在 VL 里接收一个 Tablet 实例,并在输出上返回一个 Observable<PackageArgs>:
public static class TabletHelper
{
public static IObservable<PackageArgs> PackageArrived(Tablet tablet)
{
return Observable.FromEvent<Tablet.PacketArrivalEventHandler, PackageArgs>(handler =>
{
Tablet.PacketArrivalEventHandler paHandler = (x, y, z) =>
{
handler(new PackageArgs(x, y, z));
};
return paHandler;
},
paHandler => tablet.PacketArrival += paHandler,
paHandler => tablet.PacketArrival -= paHandler);
}
}
(上游此处待补图:在 vl 里长什么样)
注意这里节点是放在 Create 上、结果存进一个数据板的,而不是放在 Update 上 —— 这样 Observable 只会被创建一次,这正是我们想要的。如果因为某些原因你必须把节点放在 Update 上(比如它输入上的 Tablet 可能会变),那么有个小技巧可以加上,用来缓存这个 Observable、只在输入变化时重建它:
public static class TabletHelper
{
public static IObservable<PackageArgs> PackageArrived(Tablet tablet)
{
return CachedObservables.GetValue(tablet, x => PackageArrived_((Tablet)x))
}
static IObservable<PackageArgs> PackageArrived_(Tablet tablet)
{
return Observable.FromEvent<Tablet.PacketArrivalEventHandler, PackageArgs>(handler =>
{
Tablet.PacketArrivalEventHandler paHandler = (x, y, z) =>
{
handler(new PackageArgs(x, y, z));
};
return paHandler;
},
paHandler => tablet.PacketArrival += paHandler,
paHandler => tablet.PacketArrival -= paHandler);
}
}