在C语言中实现一个简单的`TypedSql`结构体,我们可以定义一个结构体来表示SQL查询中的不同类型的数据。以下是一个基本的实现,它包括一个`TypedSql`结构体,它可以存储字符串、整数和浮点数类型的查询参数。```c#include #includ

摘要:前言 在 .NET 里写查询的时候,很多场景下数据其实早就都在内存里了:不是数据库连接,也不是某个远程服务的结果,而就是一个数组或者 List<T>。我只是想过滤一下、投影一下。这时候,通常有几种
前言 在 .NET 里写查询的时候,很多场景下数据其实早就都在内存里了:不是数据库连接,也不是某个远程服务的结果,而就是一个数组或者 List<T>。我只是想过滤一下、投影一下。这时候,通常有几种选择: 写一个 foreach 循环 —— 性能好、可控,但代码稍微有点啰嗦; 用 LINQ —— 写起来舒服,看起来也优雅,就是有迭代器、委托带来的那点开销; 要么干脆极端一点:把数据塞进数据库,再写真正的 SQL(这听起来就有点反直觉……) 但是我想尝试一条完全不同的思路:如果我们把 C# 的类型系统本身,当成查询计划会怎样? 也就是说,不是像平时那样: 在运行时构建一棵表达式树, 再拿着这棵树去解释执行整个查询; 而是:写一段 SQL 风格的字符串,把它编译成一个类型,这个类型从头到尾描述了整个查询管道,然后所有实际运行时的逻辑都走静态方法。 这个想法最终促成了 TypedSql —— 一个用 C# 类型系统实现的内存内 SQL 查询引擎。 把查询变成嵌套的泛型类型 TypedSql 的核心想法看上去非常简单:一个查询,其实可以是一串嵌套的泛型类型,比如 WhereSelect<TRow, …, Stop<...>> 这样。 顺着这个想法,再往下推几步,会自然落到一套具体的设计上。 把执行计划塞进类型系统 在 TypedSql 里,每一个编译好的查询,最终都会变成一个封闭的泛型管道类型。 这个管道是由一些基础节点拼出来的,比如: Where<TRow, TPredicate, TNext, TResult, TRoot> Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot> WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot> Stop<TResult, TRoot> 每个节点都实现了同一个接口: 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); } 这里可以简单理解成: Run 是外面那一圈大循环(整体遍历); Process 是对单行执行的逻辑。 比如 Where 节点大概长这样: 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[i], 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); } } } 关键点在于: 管道的形状,完全藏在这些类型参数里面; 每个节点是一个只有静态方法的 struct —— 不需要创建实例,没有虚调用。
阅读全文