跳到主要内容

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 从方案浏览器拖放

  1. 打开你想把类型放进去的那个 group
  2. 打开方案浏览器
  3. 选 “.NET Dependencies”
  4. 找到你想转发的类型
  5. 把这个类型拖放进那个 group

把类型从方案浏览器拖放进 group

3.2 手动

  1. 打开你想把类型放进去的那个 group
  2. 新建一个 Process 草图
  3. patch type 设为 “Forward”
  4. 在这个草图上设一个 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 的运算器,把它实现成返回该类型的一个实例。这往往只需要返回该类型某个构造函数的结果,别无其他。

为一个类型创建默认值

过程节点

每一个类型转发也可以直接暴露出一个过程节点。这与从普通草图里暴露一个过程节点完全一样。

  • 在转发里,转到草图浏览器,勾上 “Process Node” 复选框
  • 然后手动转发你这个 C# 类型的一个构造函数

这样你就得到了一个可用的、属于你 C# 类型的过程节点。

如果你想从同一个类型转发里暴露多于一个过程节点,那么每多一个过程节点,你就得另建一个过程定义。这些定义不转发类型,只是用该类型的运算器来搭出想要的过程。

转发运算器

如上所示,一个类型转发可以轻松地自动转发它的全部运算器。不过即便 “Forward All Nodes” 开着,手动转发某些运算器以便调整它们的针脚,仍然是讲得通的。

为个别运算器创建转发:

  1. 打开你想把这个运算器放进去的那个类型
  2. 打开方案浏览器
  3. 选 “.NET Dependencies”
  4. 找到你想导入的运算器
  5. 把这个运算器拖放进那个类型

把运算器放进类型里

备注

你也可以选中多个运算器,一次性把它们放进草图。

现在你有了一个转发运算器定义,包在你想转发的那个节点外面。被转发节点的所有针脚都会自动反映在这个转发定义的签名里。这也意味着:该节点签名的任何变动(也就是它底层的 .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);
}
}