值类型特化版字符串:ValueString#

📅 2026/7/1 1:30:31
值类型特化版字符串:ValueString#
在 .NET 里string是一个引用类型这给 TypedSql 带来了一些麻烦.NET 会对引用类型采用共享泛型在运行时做分发而不是为string泛型实例化一个具体类型这使得运行时会产生类型字典查找的开销。虽然这点开销不大但是 TypedSql 追求的是媲美手写循环的性能所以我想尽量把热路径里涉及的类型都做成值类型。于是我选择把字符串包在一个小的值类型里Copyinternal readonly struct ValueString(string? value) : IEquatableValueString, IComparableValueString { 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; }再配一个适配器把原来的string列变成ValueString列Copyinternal readonly struct ValueStringColumnTColumn, TRow : IColumnTRow, ValueString where TColumn : IColumnTRow, string { public static string Identifier TColumn.Identifier; public static ValueString Get(in TRow row) new(TColumn.Get(in row)); }在内部所有字符串列都统一成ValueString有几个好处热路径里尽量是值类型少一点引用类型的干扰避开了泛型共享带来的类型字典查找开销。对使用者来说你照样写string而我的 TypedSql 会在内部自动在边缘位置做封装/解封装所以完全透明。实现一个 SQL 子集#TypedSql 并不打算做成一个大而全的 SQL 引擎而是针对单表、内存内查询设计了一个很小的 SQL 方言支持这些语句SELECT * FROM $SELECT col FROM $SELECT col1, col2, ... FROM $WHERE支持比较,!,,,,布尔AND,OR,NOT括号字面量支持整数如42浮点数如123.45布尔true/false单引号字符串Seattle内部用转义null列名大小写不敏感$代表当前行来源整体解析流程很简单先把 SQL 字符串切成 token再构建一棵小 AST包含ParsedQuery整体查询SelectionSelectAll或者列名列表WhereExpression筛选表达式ComparisonExpression比较AndExpression与OrExpression或NotExpression非LiteralValue字面量LiteralKind.IntegerIntValueLiteralKind.FloatFloatValueLiteralKind.BooleanBoolValueLiteralKind.StringStringValuestring?LiteralKind.Null在这个阶段整个系统其实完全不知道 C# 里面的类型是什么样的列又是什么只是单纯看作 SQL 结构。类型检查、以及这个字面量能不能用在那一列上之类的问题会留到后面的编译阶段去做。把字面量变成类型 —— 包括字符串#在这里我想针对每一个 SQL 语句都生成一份独特的类型因此作为查询条件中的字面量也必须变成类型参数的一部分。于是在 TypeSql 中所有的字面量类型都实现同一个接口Copyinternal interface ILiteralT { static abstract T Value { get; } }适用范围包括整数int浮点数float字符char布尔bool字符串这里是ValueString内部包string?……未来还可以扩展更多数值字面量#数值字面量的编码方式很直接用 16 进制和位运算拼出来。先来一组IHex接口和Hex0–HexFstructCopyinternal 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; }然后一个整型字面量长这样Copyinternal readonly struct IntH7, H6, H5, H4, H3, H2, H1, H0 : ILiteralint 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; }浮点数也是一样的 8 个十六进制数位只不过最后用Unsafe.BitCastint, float转回floatCopyinternal readonly struct FloatH7, H6, H5, H4, H3, H2, H1, H0 : ILiteralfloat where H7 : IHex // ... { public static float Value Unsafe.BitCastint, float( (H7.Value 28) | (H6.Value 24) | (H5.Value 20) | (H4.Value 16) | (H3.Value 12) | (H2.Value 8) | (H1.Value 4) | H0.Value); }字符则是 4 个十六进制数位Copyinternal readonly struct CharH3, H2, H1, H0 : ILiteralchar where H3 : IHex // ... { public static char Value (char)((H3.Value 12) | (H2.Value 8) | (H1.Value 4) | H0.Value); }字符串字面量类型的链表#字符串字面量就比较有趣了。这里我选择在类型层面构建一条字符链表用接口IStringNode来描述Copyinternal interface IStringNode { static abstract int Length { get; } static abstract void Write(Spanchar destination, int index); }有三个实现StringEnd字符串的结尾长度 0StringNull表示 null 字符串长度 -1StringNodeTChar, TNext当前一个字符 剩余部分。Copyinternal readonly struct StringEnd : IStringNode { public static int Length 0; public static void Write(Spanchar destination, int index) { } } internal readonly struct StringNull : IStringNode { public static int Length -1; public static void Write(Spanchar destination, int index) { } } internal readonly struct StringNodeTChar, TNext : IStringNode where TChar : ILiteralchar where TNext : IStringNode { public static int Length 1 TNext.Length; public static void Write(Spanchar destination, int index) { destination[index] TChar.Value; TNext.Write(destination, index 1); } }有了这样的类型链表我们就可以基于某个IStringNode构造出真正的ValueStringCopyinternal readonly struct StringLiteralTString : ILiteralValueString 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[length]; TString.Write(chars.AsSpan(), 0); return new string(chars, 0, length); } } }StringLiteralTString就是一个ILiteralValueString它的Value在类型初始化时算好并缓存下来所以只需要计算一次后续访问都是直接读静态字段非常高效。把字符串塞进类型#LiteralTypeFactory.CreateStringLiteral负责把字符串字面量转换成这样一个类型Copypublic static Type CreateStringLiteral(string? value) { if (value is null) { return typeof(StringLiteralStringNull); } 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生成一个LiteralValueKind LiteralKind.StringStringValue Seattle编译阶段根据列的类型判断这是个字符串列于是对应的运行时类型是ValueString。调用CreateStringLiteral(Seattle)初始type typeof(StringEnd)从右到左遍历每个字符e→ 得到一个Char…类型4 个十六进制数位对应 Unicodetype StringNodeChare, StringEndl再往前type StringNodeCharl, StringNodeChare, StringEnd一直重复t、t、a、e、S……最终得到类似这样一个类型CopyStringNodeCharS, StringNodeChare, StringNodeChara, StringNodeChart, StringNodeChart, StringNodeCharl, StringNodeChare, StringEnd最后再用StringLiteral把它包起来CopyStringLiteral StringNodeCharS, StringNodeChare, ... 这一整个封闭泛型类型就是字面量Seattle的类型版本。而过滤器在需要值的时候只是简单地访问TLiteral.Value再通过TString.Length和TString.Write复原出一个ValueString(Seattle)其中复原通过静态类型的缓存完成借助类型系统的力量每一个独立的字面量都会产生一个单独的类型实例我们的字面量就缓存在那个类型的静态字段里从而避免了一切运行时的计算开销。null 字符串字面量#null的处理稍微特殊一点写类似WHERE Team ! null这种代码时解析器会把它识别为LiteralKind.Null对字符串列来说CreateStringLiteral(null)会返回typeof(StringLiteralStringNull)StringNull.Length -1于是StringLiteralStringNull.Value直接返回new ValueString(null)。这样一来null和在类型层面和运行时都可以被区分开。字面量工厂#上面这些编码最后都归到一个工厂类里统一封装Copyinternal 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、float、bool、ValueString