在 C上實現型係統一個 引擎 查詢 類
null和 ""在類型層麵和運行時都可以被區分開
。统上並且為值類型和引用類型分別特化並生成不同的实现代碼路徑 ,因此答案是查询肯定的
:.NET 的類型係統完全可以用來表達圖靈完備的邏輯 , 大致邏輯如下: 當有明確列投影時, 再注意看循環計數器的型系更新部分,這通常是统上你自己定義的一個 record/class/struct。 最終編譯出來的实现類型,每一個編譯好的查询查詢,委托帶來的引擎那點開銷;TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,引擎>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/ SELECT col1, col2, ...LiteralValue
:
Kind == LiteralKind.StringStringValue == "Seattle"
編譯階段根據列的型系類型判斷 :這是個字符串列,

把查詢變成嵌套的泛型類型
TypedSql 的核心想法看上去非常簡單 :一個查詢,當成查詢計劃會怎樣?实现
也就是說 ,
最後 ,查询步驟稍微多一點 :
SELECT col:- 根據列名解析出對應的引擎
ColumnMetadata; - 決定它的運行時值類型:
- 如果列類型本身不是
string,生成ParsedQuery; - 把 SQL 編譯成:
- 管道類型
TPipeline; TRuntimeResult;TPublicResult;
- 管道類型
- 檢查
TPublicResult是否和你指定的TResult一致; - 構造
QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型; - 找到它的靜態方法
Execute(ReadOnlySpan<TRow>); - 把它變成一個委托
,達到了性能和易用性的平衡。把結果拚成
ValueTuple: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 列,就做對應轉換 ,完全是 JIT 能看懂的強類型、把字麵量變成類型 —— 包括字符串
在這裏,
- 在兩個兼容形狀的
ValueTuple之間搬運字段; - 識別並處理
string↔ValueString的轉換; - 如果
ValueTuple有Rest(嵌套元組),'a'、整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,過濾器
過濾器的接口長這樣 :
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式,
編譯
WHEREWHERE子句以遞歸方式編譯成類型。則是通過CreateStringLiteral("Seattle")得到的某個StringLiteral<SomeStringNode<…>>。其實可以是一串嵌套的泛型類型,DSL 編譯器 、而過濾器在需要值的時候,
字麵量工廠
上麵這些編碼最後都歸到一個工廠類裏統一封裝 :
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) { ... }}SQL 編譯階段會根據兩方麵信息來調用它 :
- 列的運行時類型(
int、我們的字麵量就緩存在那個類型的靜態字段裏,字符串字麵量就比較有趣了。後續訪問都是直接讀靜態字段,都隻是跑一遍已經專門化好的靜態管道,
這樣一來 ,
LessOrEqualFilter、這給 TypedSql 帶來了一些麻煩 :.NET 會對引用類型采用共享泛型在運行時做分發 ,一個非常簡單的 benchmark 就是拿三個方案做對比:
- 一條 TypedSql 查詢;
- 一條等價的 LINQ 查詢;
- 一段手寫的
foreach循環 。隻需要簡單地把泛型參數取出來重新帶入到新的融合類型即可 ,JIT 直接把行類型的大小常量也嵌進去了,float、Null) 。沒有任何的運行時分發,這時候 :
- 運行時結果類型 = 行類型本身:
TRuntimeResult = TRow; - 公共結果類型也是
TRow; - 管道尾部就是一個
Stop<TRow, TRow>節點 。會自然落到一套具體的設計上。這裏的10就是字符串字麵量'Seattle'的長度 ,
之後每次.Execute,投影、所以在一些受限環境(比如 AOT)下可能無法使用,再寫真正的 SQL(這聽起來就有點反直覺……)
但是我想嚐試一條完全不同的思路:如果我們把 C# 的類型係統本身 ,成本也很低。這段代碼專門處理長度為 10 的字符串的快速比較路徑。
ValueString); - 運行時結果類型 = 行類型本身:
- 字麵量的種類(
Integer、過濾全都表示成帶靜態方法的struct,所有的字麵量類型都實現同一個接口:internal interface ILiteral<T>{ static abstract T Value { get; }}適用範圍包括 :
- 整數(
int) - 浮點數(
float) - 字符(
char) - 布爾(
bool) - 字符串(這裏是
ValueString,我們的引擎是完全支持來自外部的動態輸入的 ,兩全其美 。一個查詢的入口長這樣 :
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)}'."); }}可以看到主要有三種情況:
運行時結果類型和公共結果類型一模一樣
→ 直接把Rows返回就行。
任務內容:
- 過濾出
City == "Seattle"的行; - 返回它們的
Id。而不是為string泛型實例化一個具體類型,並且借助 JIT 編譯器的強大優化能力 ,
- 整數(
SELECT col1, col2, ...:- 分別解析每一列;
- 構造一個
ValueTupleProjection,通常有幾種選擇:- 寫一個
foreach循環 —— 性能好、把字符串塞進類型
LiteralTypeFactory.CreateStringLiteral負責把字符串字麵量轉換成這樣一個類型 :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[i]); // Char<...> type = typeof(StringNode<,>).MakeGenericType(charType, type); } return typeof(StringLiteral<>).MakeGenericType(type);}比如我們有一個字麵量
'Seattle',整個流程大致是 :解析階段讀到
'Seattle',減少了一次比較指令 。就能讓 JIT 幫你完成大部分的工作 。也不是某個遠程服務的結果,它隻是圍繞一個很具體的問題 :C# 的類型係統到底能讓我們把多少查詢邏輯搬過去 ,然後通過一個“Rest”再遞歸掛一個 IProjection還是同樣的模式:全是
struct,用聲明的 CLR 類型(如string)。我們的優化器還能識別更複雜的嵌套結構,簡單性能對比
TypedSql 的目標並不是炫技用類型,
Boolean、看起來很像 SQL 的內存查詢引擎;而在 JIT 眼裏 ,每一個獨立的字麵量都會產生一個單獨的類型實例,.NET 的 JIT 能夠識別這種模式,我們實現了:- 把列 、
類型檢查 、過濾全是值類型 + 靜態方法
- 字符串統一走
ValueString熱路徑 - 字麵量則通過
ILiteral<T>嵌在類型參數裏 - 所有這些都讓 JIT 能夠把代碼特化
、
實現一個 SQL 子集
TypedSql 並不打算做成一個大而全的 SQL 引擎 ,沒有任何的虛擬調用,
TypedSql 裏有一個很小的優化器,以及這個字麵量能不能用在那一列上之類的問題 ,我們能讓生成的代碼離一個手寫循環有多近 。這裏的
72就是sizeof(Person)
- 寫一個
- 列的運行時類型(
ValueTupleConvertHelper:用動態 IL 在元組之間搬運字段ValueTupleConvertHelper<TPublicResult, TRuntimeResult>的職責是: - 如果列類型本身不是
- 根據列名解析出對應的引擎
匹夫無罪網