TypedSql:在 C# 类型系统上实现一个 SQL 查询引擎
<h2 id="前言">前言</h2><p>在 .NET 里写查询的时候,很多场景下数据其实早就都在内存里了:不是数据库连接,也不是某个远程服务的结果,而就是一个数组或者 <code>List<T></code>。我只是想过滤一下、投影一下。这时候,通常有几种选择:</p>
<ul>
<li>写一个 <code>foreach</code> 循环 —— 性能好、可控,但代码稍微有点啰嗦;</li>
<li>用 LINQ —— 写起来舒服,看起来也优雅,就是有迭代器、委托带来的那点开销;</li>
<li>要么干脆极端一点:把数据塞进数据库,再写真正的 SQL(这听起来就有点反直觉……)</li>
</ul>
<p>但是我想尝试一条完全不同的思路:<strong>如果我们把 C# 的类型系统本身,当成查询计划会怎样?</strong></p>
<p>也就是说,不是像平时那样:</p>
<ul>
<li>在运行时构建一棵表达式树,</li>
<li>再拿着这棵树去解释执行整个查询;</li>
</ul>
<p>而是:写一段 SQL 风格的字符串,把它编译成一个<strong>类型</strong>,这个类型从头到尾描述了整个查询管道,然后所有实际运行时的逻辑都走静态方法。</p>
<p>这个想法最终促成了 TypedSql —— 一个用 C# 类型系统实现的内存内 SQL 查询引擎。</p>
<p><img src="https://img2024.cnblogs.com/blog/1590449/202511/1590449-20251124114128790-2072136532.jpg" alt="1000016101" loading="lazy"></p>
<h2 id="把查询变成嵌套的泛型类型">把查询变成嵌套的泛型类型</h2>
<p>TypedSql 的核心想法看上去非常简单:一个查询,其实可以是一串嵌套的泛型类型,比如 <code>WhereSelect<TRow, …, Stop<...>></code> 这样。</p>
<p>顺着这个想法,再往下推几步,会自然落到一套具体的设计上。</p>
<h3 id="把执行计划塞进类型系统">把执行计划塞进类型系统</h3>
<p>在 TypedSql 里,每一个编译好的查询,最终都会变成一个<strong>封闭的泛型管道类型</strong>。<br>
这个管道是由一些基础节点拼出来的,比如:</p>
<ul>
<li><code>Where<TRow, TPredicate, TNext, TResult, TRoot></code></li>
<li><code>Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot></code></li>
<li><code>WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot></code></li>
<li><code>Stop<TResult, TRoot></code></li>
</ul>
<p>每个节点都实现了同一个接口:</p>
<pre><code class="language-csharp">internal interface IQueryNode<TRow, TResult, TRoot>
{
static abstract void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime);
static abstract void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime);
}
</code></pre>
<p>这里可以简单理解成:</p>
<ul>
<li><code>Run</code> 是外面那一圈大循环(整体遍历);</li>
<li><code>Process</code> 是对单行执行的逻辑。</li>
</ul>
<p>比如 <code>Where</code> 节点大概长这样:</p>
<pre><code class="language-csharp">internal readonly struct Where<TRow, TPredicate, TNext, TResult, TRoot>
: IQueryNode<TRow, TResult, TRoot>
where TPredicate : IFilter<TRow>
where TNext : IQueryNode<TRow, TResult, TRoot>
{
public static void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime)
{
for (var i = 0; i < rows.Length; i++)
{
Process(in rows, ref runtime);
}
}
public static void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime)
{
if (TPredicate.Evaluate(in row))
{
TNext.Process(in row, ref runtime);
}
}
}
</code></pre>
<p>关键点在于:</p>
<ul>
<li>管道的<strong>形状</strong>,完全藏在这些类型参数里面;</li>
<li>每个节点是一个只有静态方法的 <code>struct</code> —— 不需要创建实例,没有虚调用。</li>
</ul>
<p>对 JIT 来说,一旦这些泛型类型参数都被代入,这就是一张普通的静态调用图而已。</p>
<h3 id="列和投影">列和投影</h3>
<p>查询总得运行在某种行类型 <code>TRow</code> 上,这通常是你自己定义的一个 record/class/struct。</p>
<p>每一列会实现这样一个接口:</p>
<pre><code class="language-csharp">internal interface IColumn<TRow, TValue>
{
static abstract string Identifier { get; }
static abstract TValue Get(in TRow row);
}
</code></pre>
<p>举个简单的例子:</p>
<pre><code class="language-csharp">internal readonly struct PersonNameColumn : IColumn<Person, string>
{
public static string Identifier => "Name";
public static string Get(in Person row) => row.Name;
}
</code></pre>
<p>而投影(<code>SELECT</code> 后面那部分)则实现:</p>
<pre><code class="language-csharp">internal interface IProjection<TRow, TResult>
{
static abstract TResult Project(in TRow row);
}
</code></pre>
<p>将选出某一列本身做成一个投影,可以这么写:</p>
<pre><code class="language-csharp">internal readonly struct ColumnProjection<TColumn, TRow, TValue>
: IProjection<TRow, TValue>
where TColumn : IColumn<TRow, TValue>
{
public static TValue Project(in TRow row) => TColumn.Get(row);
}
</code></pre>
<p>多列选择时,TypedSql 会构造专门的投影,把结果拼成 <code>ValueTuple</code>:</p>
<pre><code class="language-csharp">internal readonly struct ValueTupleProjection<TRow, TColumn1, TValue1>
: IProjection<TRow, ValueTuple<TValue1>>
where TColumn1 : IColumn<TRow, TValue1>
{
public static ValueTuple<TValue1> Project(in TRow row)
=> new(TColumn1.Get(row));
}
// … 一直到 7 列,然后通过一个“Rest”再递归挂一个 IProjection
</code></pre>
<p>还是同样的模式:全是 <code>struct</code>,全是静态方法。</p>
<h3 id="过滤器">过滤器</h3>
<p>过滤器的接口长这样:</p>
<pre><code class="language-csharp">internal interface IFilter<TRow>
{
static abstract bool Evaluate(in TRow row);
}
</code></pre>
<p>一个最常用的比较过滤器形式,是列 + 字面量:</p>
<pre><code class="language-csharp">internal readonly struct EqualsFilter<TRow, TColumn, TLiteral, TValue> : IFilter<TRow>
where TColumn : IColumn<TRow, TValue>
where TLiteral : ILiteral<TValue>
where TValue : IEquatable<TValue>, IComparable<TValue>
{
public static bool Evaluate(in TRow row)
{
if (typeof(TValue).IsValueType)
{
return TColumn.Get(row).Equals(TLiteral.Value);
}
else
{
var left = TColumn.Get(row);
var right = TLiteral.Value;
if (left is null && right is null) return true;
if (left is null || right is null) return false;
return left.Equals(right);
}
}
}
</code></pre>
<p>这里我们通过判断 <code>TValue</code> 是值类型还是引用类型,来分别处理 <code>null</code> 的情况。.NET 的 JIT 能够识别这种模式,并且为值类型和引用类型分别特化并生成不同的代码路径,从而实际上并不存在任何的分支开销。</p>
<p><code>GreaterThanFilter</code>、<code>LessThanFilter</code>、<code>GreaterOrEqualFilter</code>、<code>LessOrEqualFilter</code>、<code>NotEqualFilter</code> 等等,都是同样的套路。</p>
<p>逻辑运算也是在类型层面组合的:</p>
<pre><code class="language-csharp">internal readonly struct AndFilter<TRow, TLeft, TRight> : IFilter<TRow>
where TLeft : IFilter<TRow>
where TRight : IFilter<TRow>
{
public static bool Evaluate(in TRow row)
=> TLeft.Evaluate(in row) && TRight.Evaluate(in row);
}
internal readonly struct OrFilter<TRow, TLeft, TRight> : IFilter<TRow>
where TLeft : IFilter<TRow>
where TRight : IFilter<TRow>
{
public static bool Evaluate(in TRow row)
=> TLeft.Evaluate(in row) || TRight.Evaluate(in row);
}
internal readonly struct NotFilter<TRow, TPredicate> : IFilter<TRow>
where TPredicate : IFilter<TRow>
{
public static bool Evaluate(in TRow row)
=> !TPredicate.Evaluate(in row);
}
</code></pre>
<p>所以,一条 <code>WHERE</code> 子句,最终就会变成一棵<strong>泛型过滤器类型树</strong>,每个节点只有一个静态 <code>Evaluate</code> 方法。</p>
<h3 id="值类型特化版字符串valuestring">值类型特化版字符串:<code>ValueString</code></h3>
<p>在 .NET 里,<code>string</code> 是一个<strong>引用类型</strong>,这给 TypedSql 带来了一些麻烦:.NET 会对引用类型采用共享泛型在运行时做分发,而不是为 <code>string</code> 泛型实例化一个具体类型,这使得运行时会产生类型字典查找的开销。虽然这点开销不大,但是 TypedSql 追求的是媲美手写循环的性能,所以我想尽量把热路径里涉及的类型都做成值类型。</p>
<p>于是我选择把字符串包在一个小的值类型里:</p>
<pre><code class="language-csharp">internal readonly struct ValueString(string? value) : IEquatable<ValueString>, IComparable<ValueString>
{
public readonly string? Value = value;
public int CompareTo(ValueString other)
=> string.Compare(Value, other.Value, StringComparison.Ordinal);
public bool Equals(ValueString other)
{
return string.Equals(Value, other.Value, StringComparison.Ordinal);
}
public override string? ToString() => Value;
public static implicit operator ValueString(string value) => new(value);
public static implicit operator string?(ValueString value) => value.Value;
}
</code></pre>
<p>再配一个适配器,把原来的 <code>string</code> 列变成 <code>ValueString</code> 列:</p>
<pre><code class="language-csharp">internal readonly struct ValueStringColumn<TColumn, TRow>
: IColumn<TRow, ValueString>
where TColumn : IColumn<TRow, string>
{
public static string Identifier => TColumn.Identifier;
public static ValueString Get(in TRow row)
=> new(TColumn.Get(in row));
}
</code></pre>
<p>在内部,所有字符串列都统一成 <code>ValueString</code>,有几个好处:</p>
<ul>
<li>热路径里尽量是值类型,少一点引用类型的干扰;</li>
<li>避开了泛型共享带来的类型字典查找开销。</li>
</ul>
<p>对使用者来说,你照样写 <code>string</code>,而我的 TypedSql 会在内部自动在边缘位置做封装/解封装,所以完全透明。</p>
<h2 id="实现一个-sql-子集">实现一个 SQL 子集</h2>
<p>TypedSql 并不打算做成一个大而全的 SQL 引擎,而是针对<strong>单表、内存内</strong>查询,设计了一个很小的 SQL 方言:</p>
<p>支持这些语句:</p>
<ul>
<li><code>SELECT * FROM $</code></li>
<li><code>SELECT col FROM $</code></li>
<li><code>SELECT col1, col2, ... FROM $</code></li>
<li><code>WHERE</code> 支持:
<ul>
<li>比较:<code>=</code>, <code>!=</code>, <code>></code>, <code><</code>, <code>>=</code>, <code><=</code></li>
<li>布尔:<code>AND</code>, <code>OR</code>, <code>NOT</code></li>
<li>括号</li>
</ul>
</li>
<li>字面量支持:
<ul>
<li>整数(如 <code>42</code>)</li>
<li>浮点数(如 <code>123.45</code>)</li>
<li>布尔(<code>true</code> / <code>false</code>)</li>
<li>单引号字符串(<code>'Seattle'</code>,内部用 <code>''</code> 转义)</li>
<li><code>null</code></li>
</ul>
</li>
<li>列名大小写不敏感</li>
<li><code>$</code> 代表当前行来源</li>
</ul>
<p>整体解析流程很简单:</p>
<ol>
<li>先把 SQL 字符串切成 token;</li>
<li>再构建一棵小 AST,包含:
<ul>
<li><code>ParsedQuery</code>:整体查询</li>
<li><code>Selection</code>:<code>SelectAll</code> 或者列名列表</li>
<li><code>WhereExpression</code>:筛选表达式
<ul>
<li><code>ComparisonExpression</code>:比较</li>
<li><code>AndExpression</code>:与</li>
<li><code>OrExpression</code>:或</li>
<li><code>NotExpression</code>:非</li>
</ul>
</li>
<li><code>LiteralValue</code>:字面量
<ul>
<li><code>LiteralKind.Integer</code> + <code>IntValue</code></li>
<li><code>LiteralKind.Float</code> + <code>FloatValue</code></li>
<li><code>LiteralKind.Boolean</code> + <code>BoolValue</code></li>
<li><code>LiteralKind.String</code> + <code>StringValue</code>(<code>string?</code>)</li>
<li><code>LiteralKind.Null</code></li>
</ul>
</li>
</ul>
</li>
</ol>
<p>在这个阶段,整个系统其实<strong>完全不知道 C# 里面的类型</strong>是什么样的,列又是什么,只是单纯看作 SQL 结构。</p>
<p>类型检查、以及这个字面量能不能用在那一列上之类的问题,会留到后面的编译阶段去做。</p>
<h2 id="把字面量变成类型--包括字符串">把字面量变成类型 —— 包括字符串</h2>
<p>在这里,我想针对每一个 SQL 语句都生成一份独特的类型,因此作为查询条件中的字面量,也必须变成类型参数的一部分。</p>
<p>于是,在 TypeSql 中,所有的字面量类型都实现同一个接口:</p>
<pre><code class="language-csharp">internal interface ILiteral<T>
{
static abstract T Value { get; }
}
</code></pre>
<p>适用范围包括:</p>
<ul>
<li>整数(<code>int</code>)</li>
<li>浮点数(<code>float</code>)</li>
<li>字符(<code>char</code>)</li>
<li>布尔(<code>bool</code>)</li>
<li>字符串(这里是 <code>ValueString</code>,内部包 <code>string?</code>)</li>
<li>……未来还可以扩展更多</li>
</ul>
<h3 id="数值字面量">数值字面量</h3>
<p>数值字面量的编码方式很直接:<strong>用 16 进制和位运算拼出来</strong>。</p>
<p>先来一组 <code>IHex</code> 接口和 <code>Hex0</code>–<code>HexF</code> struct:</p>
<pre><code class="language-csharp">internal interface IHex { static abstract int Value { get; } }
internal readonly struct Hex0 : IHex { public static int Value => 0; }
// ...
internal readonly struct HexF : IHex { public static int Value => 15; }
</code></pre>
<p>然后,一个整型字面量长这样:</p>
<pre><code class="language-csharp">internal readonly struct Int<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<int>
where H7 : IHex
// ...
where H0 : IHex
{
public static int Value
=> (H7.Value << 28)
| (H6.Value << 24)
| (H5.Value << 20)
| (H4.Value << 16)
| (H3.Value << 12)
| (H2.Value <<8)
| (H1.Value <<4)
|H0.Value;
}
</code></pre>
<p>浮点数也是一样的 8 个十六进制数位,只不过最后用 <code>Unsafe.BitCast<int, float></code> 转回 <code>float</code>:</p>
<pre><code class="language-csharp">internal readonly struct Float<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<float>
where H7 : IHex
// ...
{
public static float Value
=> Unsafe.BitCast<int, float>(
(H7.Value << 28)
| (H6.Value << 24)
| (H5.Value << 20)
| (H4.Value << 16)
| (H3.Value << 12)
| (H2.Value <<8)
| (H1.Value <<4)
|H0.Value);
}
</code></pre>
<p>字符则是 4 个十六进制数位:</p>
<pre><code class="language-csharp">internal readonly struct Char<H3, H2, H1, H0> : ILiteral<char>
where H3 : IHex
// ...
{
public static char Value
=> (char)((H3.Value << 12)
| (H2.Value <<8)
| (H1.Value <<4)
|H0.Value);
}
</code></pre>
<h3 id="字符串字面量类型的链表">字符串字面量:类型的链表!</h3>
<p>字符串字面量就比较有趣了。</p>
<p>这里我选择在类型层面构建一条<strong>字符链表</strong>,用接口 <code>IStringNode</code> 来描述:</p>
<pre><code class="language-csharp">internal interface IStringNode
{
static abstract int Length { get; }
static abstract void Write(Span<char> destination, int index);
}
</code></pre>
<p>有三个实现:</p>
<ul>
<li><code>StringEnd</code>:字符串的结尾(长度 0);</li>
<li><code>StringNull</code>:表示 null 字符串(长度 -1);</li>
<li><code>StringNode<TChar, TNext></code>:当前一个字符 + 剩余部分。</li>
</ul>
<pre><code class="language-csharp">internal readonly struct StringEnd : IStringNode
{
public static int Length => 0;
public static void Write(Span<char> destination, int index) { }
}
internal readonly struct StringNull : IStringNode
{
public static int Length => -1;
public static void Write(Span<char> destination, int index) { }
}
internal readonly struct StringNode<TChar, TNext> : IStringNode
where TChar : ILiteral<char>
where TNext : IStringNode
{
public static int Length => 1 + TNext.Length;
public static void Write(Span<char> destination, int index)
{
destination = TChar.Value;
TNext.Write(destination, index + 1);
}
}
</code></pre>
<p>有了这样的类型链表,我们就可以基于某个 <code>IStringNode</code>,构造出真正的 <code>ValueString</code>:</p>
<pre><code class="language-csharp">internal readonly struct StringLiteral<TString> : ILiteral<ValueString>
where TString : IStringNode
{
public static ValueString Value => Cache.Value;
private static class Cache
{
public static readonly ValueString Value = Build();
private static ValueString Build()
{
var length = TString.Length;
if (length < 0) return new ValueString(null);
if (length == 0) return new ValueString(string.Empty);
var chars = new char;
TString.Write(chars.AsSpan(), 0);
return new string(chars, 0, length);
}
}
}
</code></pre>
<p><code>StringLiteral<TString></code> 就是一个 <code>ILiteral<ValueString></code>,它的 <code>Value</code> 在类型初始化时算好并缓存下来,所以只需要计算一次,后续访问都是直接读静态字段,非常高效。</p>
<h4 id="把字符串塞进类型">把字符串塞进类型</h4>
<p><code>LiteralTypeFactory.CreateStringLiteral</code> 负责把字符串字面量转换成这样一个类型:</p>
<pre><code class="language-csharp">public static Type CreateStringLiteral(string? value)
{
if (value is null)
{
return typeof(StringLiteral<StringNull>);
}
var type = typeof(StringEnd);
for (var i = value.Length - 1; i >= 0; i--)
{
var charType = CreateCharType(value); // Char<...>
type = typeof(StringNode<,>).MakeGenericType(charType, type);
}
return typeof(StringLiteral<>).MakeGenericType(type);
}
</code></pre>
<p>比如我们有一个字面量 <code>'Seattle'</code>,整个流程大致是:</p>
<ol>
<li>
<p>解析阶段读到 <code>'Seattle'</code>,生成一个 <code>LiteralValue</code>:</p>
<ul>
<li><code>Kind == LiteralKind.String</code></li>
<li><code>StringValue == "Seattle"</code></li>
</ul>
</li>
<li>
<p>编译阶段根据列的类型判断:这是个字符串列,于是对应的运行时类型是 <code>ValueString</code>。</p>
</li>
<li>
<p>调用 <code>CreateStringLiteral("Seattle")</code>:</p>
<ul>
<li>
<p>初始 <code>type = typeof(StringEnd)</code>;</p>
</li>
<li>
<p>从右到左遍历每个字符:</p>
<ul>
<li><code>'e'</code> → 得到一个 <code>Char<…></code> 类型(4 个十六进制数位对应 Unicode)
<ul>
<li><code>type = StringNode<Char<'e'>, StringEnd></code></li>
</ul>
</li>
<li><code>'l'</code> 再往前:
<ul>
<li><code>type = StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>></code></li>
</ul>
</li>
<li>一直重复:<code>'t'</code>、<code>'t'</code>、<code>'a'</code>、<code>'e'</code>、<code>'S'</code>……</li>
</ul>
</li>
<li>
<p>最终得到类似这样一个类型:</p>
<pre><code class="language-csharp">StringNode<Char<'S'>,
StringNode<Char<'e'>,
StringNode<Char<'a'>,
StringNode<Char<'t'>,
StringNode<Char<'t'>,
StringNode<Char<'l'>,
StringNode<Char<'e'>, StringEnd>>>>>>>>
</code></pre>
</li>
</ul>
</li>
<li>
<p>最后再用 <code>StringLiteral<></code> 把它包起来:</p>
<pre><code class="language-csharp">StringLiteral<
StringNode<Char<'S'>,
StringNode<Char<'e'>,
...
>
>
>
</code></pre>
</li>
</ol>
<p>这一整个封闭泛型类型,就是字面量 <code>'Seattle'</code> 的类型版本。</p>
<p>而过滤器在需要值的时候,只是简单地访问 <code>TLiteral.Value</code>,再通过 <code>TString.Length</code> 和 <code>TString.Write</code> 复原出一个 <code>ValueString("Seattle")</code>,其中复原通过静态类型的缓存完成,借助类型系统的力量,每一个独立的字面量都会产生一个单独的类型实例,我们的字面量就缓存在那个类型的静态字段里,从而避免了一切运行时的计算开销。</p>
<h4 id="null-字符串字面量">null 字符串字面量</h4>
<p><code>null</code> 的处理稍微特殊一点:</p>
<ul>
<li>写类似 <code>WHERE Team != null</code> 这种代码时,解析器会把它识别为 <code>LiteralKind.Null</code>;</li>
<li>对字符串列来说,<code>CreateStringLiteral(null)</code> 会返回 <code>typeof(StringLiteral<StringNull>)</code>;</li>
<li><code>StringNull.Length == -1</code>,于是 <code>StringLiteral<StringNull>.Value</code> 直接返回 <code>new ValueString(null)</code>。</li>
</ul>
<p>这样一来,<code>null</code> 和 <code>""</code> 在类型层面和运行时都可以被区分开。</p>
<h3 id="字面量工厂">字面量工厂</h3>
<p>上面这些编码最后都归到一个工厂类里统一封装:</p>
<pre><code class="language-csharp">internal static class LiteralTypeFactory
{
public static Type CreateIntLiteral(int value) { ... }
public static Type CreateFloatLiteral(float value) { ... }
public static Type CreateBoolLiteral(bool value) { ... }
public static Type CreateStringLiteral(string? value) { ... }
}
</code></pre>
<p>SQL 编译阶段会根据两方面信息来调用它:</p>
<ul>
<li>列的运行时类型(<code>int</code>、<code>float</code>、<code>bool</code>、<code>ValueString</code>);</li>
<li>字面量的种类(<code>Integer</code>、<code>Float</code>、<code>Boolean</code>、<code>String</code>、<code>Null</code>)。</li>
</ul>
<p>最终的效果就是:<strong>WHERE 子句里每一个字面量,都会变成一个具体的 <code>ILiteral<T></code> 类型,值直接嵌在类型参数里。</strong></p>
<h2 id="搭好整个管道类型">搭好整个管道类型</h2>
<p>到目前为止,我们已经有了:</p>
<ul>
<li>一棵解析出来的查询(<code>SELECT</code> + <code>WHERE</code>);</li>
<li>一份 schema,把列名映射到具体的 <code>IColumn<TRow, TValue></code> 实现;</li>
<li>一套机制,把字面量变成 <code>ILiteral<T></code> 类型。</li>
</ul>
<p>SQL 编译器接下来要做的就是,把这些东西变成:</p>
<ul>
<li>一个封闭的管道类型 <code>TPipeline</code>,它实现 <code>IQueryNode<TRow, TRuntimeResult, TRoot></code>;</li>
<li>一个运行时结果类型 <code>TRuntimeResult</code>;</li>
<li>一个对外公开的结果类型 <code>TPublicResult</code>。</li>
</ul>
<h3 id="编译-select">编译 <code>SELECT</code></h3>
<p>先看选择部分。</p>
<h4 id="select-"><code>SELECT *</code></h4>
<p>最简单的情况就是:<code>SELECT * FROM $</code>。</p>
<p>这时候:</p>
<ul>
<li>运行时结果类型 = 行类型本身:<code>TRuntimeResult = TRow</code>;</li>
<li>公共结果类型也是 <code>TRow</code>;</li>
<li>管道尾部就是一个 <code>Stop<TRow, TRow></code> 节点。</li>
</ul>
<p>大致逻辑如下:</p>
<pre><code class="language-csharp">TRuntimeResult = typeof(TRow);
TPublicResult = typeof(TRow);
TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));
</code></pre>
<h4 id="select-col--select-col1-col2-"><code>SELECT col</code> / <code>SELECT col1, col2, ...</code></h4>
<p>当有明确列投影时,步骤稍微多一点:</p>
<ul>
<li>
<p><code>SELECT col</code>:</p>
<ul>
<li>根据列名解析出对应的 <code>ColumnMetadata</code>;</li>
<li>决定它的运行时值类型:
<ul>
<li>如果列类型本身不是 <code>string</code>,运行时类型就跟它一致;</li>
<li>如果是 <code>string</code>,运行时类型改为 <code>ValueString</code>;</li>
</ul>
</li>
<li>构建一个 <code>ColumnProjection<TRuntimeColumn, TRow, TRuntimeValue></code>。</li>
</ul>
</li>
<li>
<p><code>SELECT col1, col2, ...</code>:</p>
<ul>
<li>分别解析每一列;</li>
<li>构造一个 <code>ValueTupleProjection</code>,返回一个 <code>ValueTuple<...></code>,里面放运行时类型;</li>
<li>同时记录一份公共 <code>ValueTuple<...></code> 类型,用声明的 CLR 类型(如 <code>string</code>)。</li>
</ul>
</li>
</ul>
<p>最后,无论是一列还是多列,都会在 <code>Stop</code> 前面再加一个 <code>Select</code> 节点:</p>
<pre><code class="language-text">Select<TRow, TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>
</code></pre>
<p>这个节点内部会调用投影的静态 <code>Project</code> 方法,再把结果转交给 <code>Stop.Process</code> 处理。</p>
<h3 id="编译-where">编译 <code>WHERE</code></h3>
<p><code>WHERE</code> 子句以递归方式编译成类型。</p>
<h4 id="布尔结构">布尔结构</h4>
<p>给定一个解析后的 <code>WhereExpression</code> 树:</p>
<ul>
<li><code>A AND B</code> → <code>AndFilter<TRow, TA, TB></code>;</li>
<li><code>A OR B</code> → <code>OrFilter<TRow, TA, TB></code>;</li>
<li><code>NOT A</code> → <code>NotFilter<TRow, TA></code>。</li>
</ul>
<p>编译器做的事情,大概是对这棵树一层层往下调自己的方法:</p>
<pre><code class="language-csharp">Type BuildPredicate<TRow>(WhereExpression expr)
{
return expr switch
{
ComparisonExpression cmpExpr => BuildComparisonPredicate<TRow>(cmpExpr),
AndExpression andExpr => typeof(AndFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(andExpr.Left), BuildPredicate<TRow>(andExpr.Right)),
OrExpression orExpr => typeof(OrFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(orExpr.Left), BuildPredicate<TRow>(orExpr.Right)),
NotExpression notExpr => typeof(NotFilter<,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(notExpr.Expression)),
_ => throw …
};
}
</code></pre>
<h4 id="比较表达式">比较表达式</h4>
<p>每一个叶子比较表达式,比如:</p>
<pre><code class="language-sql">City = 'Seattle'
Salary >= 180000
Team != null
</code></pre>
<p>都会变成一个具体的过滤器类型:</p>
<pre><code class="language-csharp">Type BuildComparisonPredicate<TRow>(ComparisonExpression comparison)
{
var rowType = typeof(TRow);
var column = SchemaRegistry<TRow>.ResolveColumn(comparison.ColumnIdentifier);
var runtimeColumnType = column.GetRuntimeColumnType(rowType);
var runtimeColumnValueType = column.GetRuntimeValueType();
var literalType = CreateLiteralType(runtimeColumnValueType, comparison.Literal);
var filterDefinition = comparison.Operator switch
{
ComparisonOperator.Equals => typeof(EqualsFilter<,,,>),
ComparisonOperator.GreaterThan => typeof(GreaterThanFilter<,,,>),
ComparisonOperator.LessThan => typeof(LessThanFilter<,,,>),
ComparisonOperator.GreaterOrEqual=> typeof(GreaterOrEqualFilter<,,,>),
ComparisonOperator.LessOrEqual => typeof(LessOrEqualFilter<,,,>),
ComparisonOperator.NotEqual => typeof(NotEqualFilter<,,,>),
_ => throw …
};
return filterDefinition.MakeGenericType(
rowType, runtimeColumnType, literalType, runtimeColumnValueType);
}
</code></pre>
<p>以 <code>City = 'Seattle'</code> 为例,如果那一列是字符串列,那么:</p>
<ul>
<li>运行时列类型是:<code>ValueStringColumn<PersonCityColumn, Person></code>;</li>
<li>运行时值类型是:<code>ValueString</code>;</li>
<li>字面量类型,则是通过 <code>CreateStringLiteral("Seattle")</code> 得到的某个 <code>StringLiteral<SomeStringNode<…>></code>。</li>
</ul>
<p>最后组合出一个过滤器类型:</p>
<pre><code class="language-csharp">EqualsFilter<Person,
ValueStringColumn<PersonCityColumn, Person>,
StringLiteral<...>,
ValueString>
</code></pre>
<p>到这一步,我们就可以把一个 <code>Where</code> 节点挂到管道上了:</p>
<pre><code class="language-csharp">Where<TRow, TPredicate, TNext, TRuntimeResult, TRoot> → ...
</code></pre>
<h3 id="把-where-和-select-融合起来">把 <code>Where</code> 和 <code>Select</code> 融合起来</h3>
<p>直接这么拼出来的管道是正确的,但在性能上还能再优化一点:<br>
<code>Where</code> 和 <code>Select</code> 其实可以合并成一步。</p>
<p>TypedSql 里有一个很小的优化器,会去找这样的模式:</p>
<ul>
<li><code>Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot></code></li>
</ul>
<p>一旦发现,就把它替换成:</p>
<pre><code class="language-csharp">WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>
</code></pre>
<p>这个融合节点的实现如下:</p>
<pre><code class="language-csharp">internal readonly struct WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>
: IQueryNode<TRow, TResult, TRoot>
where TPredicate : IFilter<TRow>
where TProjection : IProjection<TRow, TMiddle>
where TNext : IQueryNode<TMiddle, TResult, TRoot>
{
public static void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime)
{
for (var i = 0; i < rows.Length; i++)
{
Process(in rows, ref runtime);
}
}
public static void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime)
{
if (TPredicate.Evaluate(in row))
{
var projected = TProjection.Project(in row);
TNext.Process(in projected, ref runtime);
}
}
}
</code></pre>
<p>于是像下面这种常见的查询:</p>
<pre><code class="language-sql">SELECT Name FROM $ WHERE City = 'Seattle'
</code></pre>
<p>最终就会是:</p>
<pre><code class="language-text">WhereSelect<...> → Stop<...>
</code></pre>
<p>也就是说:<strong>一个循环里完成过滤和投影</strong>,不需要再分两趟。并且,我们的优化器还能识别更复杂的嵌套结构,尽可能地把 <code>Where</code> 和 <code>Select</code> 融合在一起,减少中间步骤,提升性能。而这并不需要复杂的优化算法,只需要简单地把泛型参数取出来重新带入到新的融合类型即可,实现起来非常简单。</p>
<h2 id="结果转换">结果转换</h2>
<p>管道把所有行跑完之后,最后还得把结果以某种形式“交出去”。</p>
<p>一个查询的入口长这样:</p>
<pre><code class="language-csharp">internal static class QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>
where TPipeline : IQueryNode<TRow, TRuntimeResult, TRow>
{
public static IReadOnlyList<TPublicResult> Execute(ReadOnlySpan<TRow> rows)
{
var runtime = new QueryRuntime<TRuntimeResult>(rows.Length);
TPipeline.Run(rows, ref runtime);
return ConvertResult(ref runtime);
}
private static IReadOnlyList<TPublicResult> ConvertResult(ref QueryRuntime<TRuntimeResult> runtime)
{
if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<TPublicResult>))
{
return (IReadOnlyList<TPublicResult>)(object)runtime.Rows;
}
else if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<ValueString>) && typeof(IReadOnlyList<TPublicResult>) == typeof(IReadOnlyList<string>))
{
return (IReadOnlyList<TPublicResult>)(object)runtime.AsStringRows();
}
else if (RuntimeFeature.IsDynamicCodeSupported && typeof(TRuntimeResult).IsGenericType && typeof(TPublicResult).IsGenericType)
{
return runtime.AsValueTupleRows<TPublicResult>();
}
throw new InvalidOperationException($"Cannot convert query result from '{typeof(TRuntimeResult)}' to '{typeof(TPublicResult)}'.");
}
}
</code></pre>
<p>可以看到主要有三种情况:</p>
<ol>
<li>
<p><strong>运行时结果类型和公共结果类型一模一样</strong><br>
→ 直接把 <code>Rows</code> 返回就行。</p>
</li>
<li>
<p><strong>运行时内部用的是 <code>ValueString</code>,外面希望看到 <code>string</code></strong><br>
→ 调用 <code>AsStringRows</code>,它会把内部的 <code>ValueString[]</code> 包装一下,对外返回 <code>string?</code>(靠隐式转换)。</p>
</li>
<li>
<p><strong>两边都是某种 <code>ValueTuple</code> 形状</strong><br>
→ 用 <code>AsValueTupleRows<TPublicResult>()</code>,底层交给 <code>ValueTupleConvertHelper</code> 去做拷贝和字段转换。</p>
</li>
</ol>
<h3 id="valuetupleconverthelper用动态-il-在元组之间搬运字段"><code>ValueTupleConvertHelper</code>:用动态 IL 在元组之间搬运字段</h3>
<p><code>ValueTupleConvertHelper<TPublicResult, TRuntimeResult></code> 的职责是:</p>
<ul>
<li>在两个兼容形状的 <code>ValueTuple</code> 之间搬运字段;</li>
<li>识别并处理 <code>string</code> ↔ <code>ValueString</code> 的转换;</li>
<li>如果 <code>ValueTuple</code> 有 <code>Rest</code>(嵌套元组),要递归下去做同样的事情。</li>
</ul>
<p>它在类型初始化时,会生成一个 <code>DynamicMethod</code> 来做拷贝:</p>
<pre><code class="language-csharp">internal static class ValueTupleConvertHelper<TPublicResult, TRuntimeResult>
{
private delegate void CopyDelegate(ref TPublicResult dest, ref readonly TRuntimeResult source);
private static readonly CopyDelegate _helper = default!;
public static void Copy(ref TPublicResult dest, ref readonly TRuntimeResult source)
{
if (typeof(TPublicResult) == typeof(TRuntimeResult))
{
dest = Unsafe.As<TRuntimeResult, TPublicResult>(ref Unsafe.AsRef(in source));
}
else
{
_helper.Invoke(ref dest, in source);
}
}
static ValueTupleConvertHelper()
{
// 构造 DynamicMethod 和 IL,按字段复制,
// 若发现 string <-> ValueString,就做对应转换,
// 遇到 Rest 字段时递归。
}
}
</code></pre>
<p>这样,运行时内部可以用一个对自己更舒服的元组类型,比如 <code>(ValueString, int, ValueString, …)</code>,而外面看到的则是 <code>(string, int, string, …)</code>,两者之间通过这一层帮助类桥接,成本也很低。这使得查询过程可以最大化利用值类型的泛型特化优势,同时对外还不需要暴露这些内部细节,达到了性能和易用性的平衡。</p>
<p>不过需要注意的是,这一块用到了动态代码生成,所以在一些受限环境(比如 AOT)下可能无法使用,因此 TypedSql 会在编译阶段检查这一点,确保只有在支持动态代码的环境下,才允许使用这种元组转换。否则的话,就只能退回到直接让运行时结果类型和公共结果类型一致的方式。</p>
<h2 id="整体流程编译并执行查询">整体流程:编译并执行查询</h2>
<p>站在使用者的角度,入口一般会是这样的:</p>
<pre><code class="language-csharp">var compiled = QueryEngine.Compile<Person, string>(
"SELECT Name FROM $ WHERE City != 'Seattle'");
</code></pre>
<p><code>Compile<TRow, TResult></code> 在内部会做这么几件事:</p>
<ol>
<li>解析 SQL,生成 <code>ParsedQuery</code>;</li>
<li>把 SQL 编译成:
<ul>
<li>管道类型 <code>TPipeline</code>;</li>
<li><code>TRuntimeResult</code>;</li>
<li><code>TPublicResult</code>;</li>
</ul>
</li>
<li>检查 <code>TPublicResult</code> 是否和你指定的 <code>TResult</code> 一致;</li>
<li>构造 <code>QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult></code> 这个类型;</li>
<li>找到它的静态方法 <code>Execute(ReadOnlySpan<TRow>)</code>;</li>
<li>把它变成一个委托,塞进 <code>CompiledQuery<TRow, TResult></code>。</li>
</ol>
<p><code>CompiledQuery<TRow, TResult></code> 本身只是包了一个委托:</p>
<pre><code class="language-csharp">private readonly Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>> _entryPoint
= executeMethod.CreateDelegate<Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>>>();
</code></pre>
<p>然后对外暴露:</p>
<pre><code class="language-csharp">public IReadOnlyList<TResult> Execute(ReadOnlySpan<TRow> rows)
=> _entryPoint(rows);
</code></pre>
<p>得益于 .NET 10 对委托的逃逸分析、去虚拟化和内联等优化,这一层委托调用可以说几乎没有任何开销。</p>
<p>在 JIT 看来,一旦 <code>Compile</code> 做完这些准备工作,以后每次 <code>Execute</code> 就只是:</p>
<ul>
<li>一次直接的静态调用;</li>
<li>调入一个所有类型参数已经封死的泛型方法;</li>
<li>这个方法里面再调用一串全是 <code>struct</code> 和静态方法组成的管道。</li>
</ul>
<p>最终编译出来的类型,你既可以直接拿去执行,也可以把它输出到代码里然后通过 NativeAOT 编译成原生二进制文件,一套代码同时支持 JIT 和 AOT!</p>
<h2 id="使用和性能测试">使用和性能测试</h2>
<h3 id="快速上手">快速上手</h3>
<p>和很多轻量级查询库类似,TypedSql 的打开方法是:</p>
<ol>
<li>
<p>定义你的行类型,例如:</p>
<pre><code class="language-csharp">public sealed record Person(
int Id,
string Name,
int Age,
string City,
float Salary,
string Department,
bool IsManager,
int YearsAtCompany,
string Country,
string? Team,
string Level);
</code></pre>
</li>
<li>
<p>为每一列实现一个 <code>IColumn<Person, TValue></code>;</p>
</li>
<li>
<p>把这些列注册到 <code>Person</code> 对应的 schema 里;</p>
</li>
<li>
<p>然后就可以编译并运行查询,例如:</p>
<pre><code class="language-csharp">// 编译一次
var wellPaidManagers = QueryEngine.Compile<Person, Person>(
"""
SELECT * FROM $
WHERE Department = 'Engineering'
AND IsManager = true
AND YearsAtCompany >= 5
AND Salary > 170000
AND Country = 'US'
""");
// 针对不同数据集多次执行
var result = wellPaidManagers.Execute(allPeople.AsSpan());
</code></pre>
</li>
</ol>
<p>要是你只需要一部分列,也可以返回元组:</p>
<pre><code class="language-csharp">var seniorTitles = QueryEngine.Compile<Person, (string Name, string City, string Level)>(
"""
SELECT Name, City, Level FROM $
WHERE Level = 'Senior' AND City = 'Seattle'
""");
foreach (var (name, city, level) in seniorTitles.Execute(allPeople.AsSpan()))
{
Console.WriteLine($"{name} in {city} [{level}]");
}
</code></pre>
<p>所有重活——解析 SQL、字面量编码、在类型系统里搭管道——都发生在编译查询这一步。<br>
之后每次 <code>.Execute</code>,都只是跑一遍已经专门化好的静态管道,没有任何的运行时分发,没有任何的虚拟调用,不存在任何的反射和装箱,完全是 JIT 能看懂的强类型、零分配代码,从而实现极高的性能。</p>
<h3 id="简单性能对比">简单性能对比</h3>
<p>TypedSql 的目标并不是炫技用类型,而是想试试看:在保持 SQL 风格外壳的情况下,我们能让生成的代码离一个手写循环有多近。</p>
<p>一个非常简单的 benchmark 就是拿三个方案做对比:</p>
<ul>
<li>一条 TypedSql 查询;</li>
<li>一条等价的 LINQ 查询;</li>
<li>一段手写的 <code>foreach</code> 循环。</li>
</ul>
<p>任务内容:</p>
<ul>
<li>过滤出 <code>City == "Seattle"</code> 的行;</li>
<li>返回它们的 <code>Id</code>。</li>
</ul>
<p>TypedSql 编译出来的类型大概是这样:</p>
<pre><code class="language-csharp">QueryProgram<
Person,
WhereSelect<
Person,
EqualsFilter<
Person,
ValueStringColumn<PersonCityColumn, Person>,
'Seattle',
ValueString
>,
ColumnProjection<PersonIdColumn, Person, Int32>,
Stop<Int32, Person>,
Int32,
Int32,
Person>,
Int32,
Int32
>
</code></pre>
<p>让我们来看看 RyuJIT 为我们的查询方案生成了什么样的机器码:</p>
<pre><code class="language-nasm">G_M000_IG01: ; prologue
push r15
push r14
push rdi
push rsi
push rbp
push rbx
sub rsp, 40
mov rbx, rcx
G_M000_IG02: ; 分配结果数组
mov esi, dword ptr
mov edx, esi
mov rcx, 0x7FFE71F29558
call CORINFO_HELP_NEWARR_1_VC
mov rdi, rax
xor ebp, ebp
mov rbx, bword ptr
test esi, esi
jle SHORT G_M000_IG06
G_M000_IG03: ; 初始化循环变量
xor r14d, r14d
G_M000_IG04: ; 循环体
lea r15, bword ptr
mov rcx, gword ptr
mov rdx, 0x16EB0400D30
mov rdx, gword ptr
mov rdx, gword ptr
cmp rcx, rdx
je G_M000_IG12
test rcx, rcx
je SHORT G_M000_IG05
test rdx, rdx
je SHORT G_M000_IG05
mov r8d, dword ptr
cmp r8d, dword ptr
je SHORT G_M000_IG08
G_M000_IG05: ; 更新循环计数器
add r14, 72
dec esi
jne SHORT G_M000_IG04
G_M000_IG06: ; 产生结果对象
mov rcx, 0x7FFE72227600
call CORINFO_HELP_NEWSFAST
mov rbx, rax
lea rcx, bword ptr
mov rdx, rdi
call CORINFO_HELP_ASSIGN_REF
mov dword ptr , ebp
mov rax, rbx
G_M000_IG07: ; epilogue
add rsp, 40
pop rbx
pop rbp
pop rsi
pop rdi
pop r14
pop r15
ret
G_M000_IG08: ; 字符串长度比较
lea rax, bword ptr
add rdx, 12
mov ecx, dword ptr
add ecx, ecx
mov r8d, ecx
cmp r8, 10
je SHORT G_M000_IG10
G_M000_IG09: ; 字符串内容慢速比较
mov rcx, rax
call
jmp SHORT G_M000_IG11
G_M000_IG10: ; 字符串内容快速比较
mov rcx, qword ptr
mov rax, qword ptr
mov r8, qword ptr
xor rcx, r8
xor rax, qword ptr
or rcx, rax
sete al
movzx rax, al
G_M000_IG11: ; 处理比较结果
test eax, eax
je SHORT G_M000_IG05
G_M000_IG12: ; 把匹配的 Id 写入结果数组
mov ecx, dword ptr
lea rax, bword ptr
lea edx,
mov r15d, edx
movsxd rdx, ebp
mov dword ptr , ecx
mov ebp, r15d
jmp G_M000_IG05
</code></pre>
<p>注意看 <code>G_M000_IG08</code> 的 <code>r8, 10</code>,这里的 <code>10</code> 就是字符串字面量 <code>'Seattle'</code> 的长度,JIT 直接把我们的字符串字面量的长度常量嵌进了机器码里;进一步当长度匹配时,JIT 又生成了代码跳转到 <code>G_M000_IG10</code>,这段代码专门处理长度为 10 的字符串的快速比较路径。也就是说,JIT 不仅把字面量的值嵌进去了,还根据它生成了专门的代码路径!</p>
<p>再注意看循环计数器的更新部分,<code>G_M000_IG05</code> 里的 <code>add r14, 72</code>,这里的 <code>72</code> 就是 <code>sizeof(Person)</code>,JIT 直接把行类型的大小常量也嵌进去了,避免了运行时的计算;而 <code>dec esi</code> 更是直接把递增的循环优化成了递减,减少了一次比较指令。</p>
<p>上述代码的逻辑等价于:</p>
<pre><code class="language-csharp">int length = elements.Length;
Span<int> values = new int;
int count = 0;
for (int i = length - 1; i >= 0; i--)
{
var elem = elements;
var city = elem.City;
if (city == null)
continue;
if (city.Length == 10 && city == "Seattle")
{
values = elem.Id;
count++;
}
}
return values[..count];
</code></pre>
<p>看到了吗?跟你手写的循环几乎一模一样!我们的抽象完全被 JIT 优化的一干二净!</p>
<p>上个跑分结果:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th style="text-align: right">Mean</th>
<th style="text-align: right">Error</th>
<th style="text-align: right">StdDev</th>
<th style="text-align: right">Gen0</th>
<th style="text-align: right">Code Size</th>
<th style="text-align: right">Allocated</th>
</tr>
</thead>
<tbody>
<tr>
<td>TypedSql</td>
<td style="text-align: right">10.953 ns</td>
<td style="text-align: right">0.0250 ns</td>
<td style="text-align: right">0.0195 ns</td>
<td style="text-align: right">0.0051</td>
<td style="text-align: right">111 B</td>
<td style="text-align: right">80 B</td>
</tr>
<tr>
<td>Linq</td>
<td style="text-align: right">27.030 ns</td>
<td style="text-align: right">0.1277 ns</td>
<td style="text-align: right">0.1067 ns</td>
<td style="text-align: right">0.0148</td>
<td style="text-align: right">3,943 B</td>
<td style="text-align: right">232 B</td>
</tr>
<tr>
<td>Foreach</td>
<td style="text-align: right">9.429 ns</td>
<td style="text-align: right">0.0417 ns</td>
<td style="text-align: right">0.0326 ns</td>
<td style="text-align: right">0.0046</td>
<td style="text-align: right">407 B</td>
<td style="text-align: right">72 B</td>
</tr>
</tbody>
</table>
<p>可以看到:<strong>TypedSql 在时间和分配上无限逼近 <code>foreach</code>,远远超过即使是在 .NET 10 中已经被高度优化后的 LINQ 的性能。</strong></p>
<p>这也符合我们对它内部结构的预期:</p>
<ul>
<li>查询管道是类型层级的,结构在编译期就定死</li>
<li>列、投影、过滤全是值类型 + 静态方法</li>
<li>字符串统一走 <code>ValueString</code> 热路径</li>
<li>字面量则通过 <code>ILiteral<T></code> 嵌在类型参数里</li>
<li>所有这些都让 JIT 能够把代码特化、展开、内联,最终生成和手写循环几乎一样的机器码</li>
</ul>
<h2 id="尾声">尾声</h2>
<p>TypedSql 只是一个简单的内存查询引擎实验。它只是围绕一个很具体的问题:<strong>C# 的类型系统到底能让我们把多少查询逻辑搬过去,.NET 又能针对这些类型生成多快的代码?</strong></p>
<p>于是,在 TypeSql 中,我们实现了:</p>
<ul>
<li>把列、投影、过滤全都表示成带静态方法的 <code>struct</code>,并通过接口的静态抽象成员来约束它们的行为</li>
<li>把它们组合成一串嵌套的泛型管道节点(<code>Where</code>、<code>Select</code>、<code>WhereSelect</code>、<code>Stop</code>)</li>
<li>把数字和字符串字面量都编码成类型(<code>ILiteral<T></code>)</li>
</ul>
<p>最后得到的是一个小小的、看起来很像 SQL 的内存查询引擎;而在 JIT 眼里,它其实就是一套可以进行高度优化的、类型特化后的循环。</p>
<p>因此答案是肯定的:<strong>.NET 的类型系统完全可以用来表达图灵完备的逻辑,并且借助 JIT 编译器的强大优化能力,生成非常高效的代码。</strong></p>
<p>展望未来的应用,诸如查询引擎、DSL 编译器、甚至是语言运行时等复杂系统,都可以通过类似的方式来实现,从而在保持灵活性的同时,最大化性能。而你甚至不需要实现任何的代码生成后端,只要利用好 C# 的泛型和静态成员,就能让 JIT 帮你完成大部分的工作。而把构建好的类型输出成代码文件,再通过 NativeAOT 编译成原生二进制文件,也同样是可行的。编写一次,同时支持 JIT 和 AOT,两全其美。并且不同于 C++ 的模板和 constexpr,我们的引擎是完全支持来自外部的动态输入的,而不需要在编译时确定一切!</p>
<p>本项目的代码已经开源在 GitHub 上,欢迎点赞和 Star:https://github.com/hez2010/TypedSql</p><br><br>
来源:https://www.cnblogs.com/hez2010/p/19261972/turning-csharp-type-system-into-a-query-engine
頁:
[1]