深入探究linq原理——如何在自己的语言里实现linq

坑挖的有点多,最近打算填一个:给scala加上linq。

在spark RDD和DataFrame上直接用岂不是美滋滋。

用过几次c#,linq还是非常直观的,很喜欢这个设计。不过现在都忘的差不多了,再来回顾一下linq到底是个什么东西。

overview

先不讲类型签名扩展方法这些细节,我们从从官网给的最基本的例子开始,来一个整体的概览,看看linq到底是什么:

using System;
using System.Linq;

class IntroToLINQ
{        
    static void Main()
    {
        // The Three Parts of a LINQ Query:
        //  1. Data source.
        int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };

        // 2. Query creation.
        // numQuery is an IEnumerable<int>
        var numQuery =
            from num in numbers
            where (num % 2) == 0
            select num;

        // 3. Query execution.
        foreach (int num in numQuery)
        {
            Console.Write("{0,1} ", num);
        }
    }
}

linq,语言集成查询,就是语法上支持类sql的查询语法,对于熟悉sql查询的广大coder,可读性比链式方法调用不知高到哪里去了。

但这也只是一层语法糖而已,在编译后还是要转化成方法调用。

比如上面的查询等价于:

var numQuery = numbers.Where(num => num %2 ==0).Select(num => num);

当然因为select的数据没变,这个Select调用完全可以省略。而Where就相当于filter,Select就相当于map,这些简单的操作都非常容易理解。linq支持的其他join、aggerate等操作符,同样是写好的一堆方法,比较容易理解。

比较特殊的是:当多个from串联在一起时,事情就变得稍微有些复杂。下面具体介绍一下这种情况。

from和SelectMany

还是先给例子:

using System;
using System.Linq;

class IntroToLINQ
{
    static void Main ()
    {
        int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };

        var numQuery =
            from num0 in numbers
            from num1 in numbers
            where  num0 + num1 > 11
            select new  {num0, num1};

        foreach (var num in numQuery) {
            Console.Write ("{0,1} ", num);
        }
    }
}

只看查询语句,它干了什么非常容易理解:在numbers和numbers(自己和自己)的笛卡尔积中选择出两数之和大于11的组合,输出是:

{ num0 = 5, num1 = 6 }
{ num0 = 6, num1 = 5 }
{ num0 = 6, num1 = 6 }

提一些稍微扩展的内容,看不懂没关系,可以跳过,如果有兴趣搞懂的话可以了解一下haskell的Monad:

看着一段scala的代码:

object Main {
  def main(args: Array[String]): Unit = {
    val numbers = List(0, 1, 2, 3, 4, 5, 6)

    val queryResult =
      for (num0 <- numbers; num1 <- numbers
           if num0 + num1 > 10) yield (num0, num1)

    queryResult.foreach(println)
  }
}

输出结果:

(5,6)
(6,5)
(6,6)

然后是一段可以在ghci里执行的haskell代码:

numbers = [0, 1, 2, 3, 4, 5, 6]

queryResult = do {
  num0<-numbers;
  num1<-numbers;
  if num0+num1>10
    then [(num0,num1)]
    else []
}

haskell没有foreach,手动看一下结果

> queryResult

得到

=> [(5,6),(6,5),(6,6)]

可以发现得到的结果是一模一样的,事实上这三者本来就是一回事。c#串联的from查询表达式、scala的for语法、haskell的do notation,本质上都是一个东西,都是一层语法糖,把对Monad的操作串联起来,最后都会翻译成方法/函数调用。其底层方法/函数分别是SelectManyflatMap>>=。至于Monad是个什么东西,又是另一个话题了,说大不大说小不小的话题。。。

还是回到正题,我们来看一下数组这些容器的SelectMany方法的具体功能。

接受一个函数的SelectMany

先看一个最普通的SelectMany例子,这也是它原本的语义:

int[] list = { 1, 2, 3 };

Func<int, string[]> selector = x =>
{
    var s = String.Format("hi{0}", x);
    return new[] { s, s, s };
};

var result = list.SelectMany(selector);

foreach (var e in result)
{
    Console.Write("{0},", e);
}

输出是:

hi1,hi1,hi1,hi2,hi2,hi2,hi3,hi3,hi3,

很容易理解,SelectMany接受一个函数,这个函数对容器的每个元素应用一遍,每次都返回一个新的容器。比如这里,selector接受数字N然后返回{"hiN","hiN","hiN"}这个列表,对每个元素调用一遍就会得到这个大列表{{"hi1","hi1","hi1"},{"hi2","hi2","hi2"},{"hi3","hi3","hi3"}},最后把这个大列表拍平就得到最终的结果:{hi1,hi1,hi1,hi2,hi2,hi2,hi3,hi3,hi3}。当然实际的实现不一定这样来,但是这样理解就对了。

让我们看一下这个函数的签名:

public static IEnumerable<TResult> SelectMany<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, IEnumerable<TResult>> selector
);

和例子的类型是对应的。

没接触过c#的话需要注意两点:

  1. 第一个参数source前面有个this,这是扩展方法的语法,source.SelectMany(selector)就相当于SelectMany(source,selector)
  2. IEnumerable<T>是一个接口,c#的数组都实现了这个接口。所以int[]满足IEnumerable<int>string[]满足IEnumerable<string>

这个SelectMany有什么用呢?

前面说过了,它能把一系列操作串联起来。我们再来看一个例子,求两个列表的笛卡尔积,比如对于[1,2]he[3,4]我们怎么得到[(1,3),(1,4),(2,3),(2,4)]

int[] alist = { 1, 2 };
int[] blist = { 3, 4 };

var result = alist.SelectMany(
    a => blist.Select(
        b => new { a, b }
    )
);

foreach (var e in result)
{
    Console.Write("{0},", e);
}

输出是:{ a = 1, b = 3 },{ a = 1, b = 4 },{ a = 2, b = 3 },{ a = 2, b = 4 },

怎么理解呢,看里面函数的功能就可以了,它对于alist的一个元素a,会将其和blist的每个元素组合一次最后生成[(a,3),(a,4)]。对alist里的每个a都来一遍这个函数就得到[ [(a0,3),(a0,4)], [(a1,3),(a1,4)] ],把它拍平就是最后的结果了。

然后以此类推,求三个列表的笛卡尔积:

int[] alist = { 1, 2 };
int[] blist = { 3, 4 };
int[] clist = { 4, 5 };

var result = alist.SelectMany(
    a => blist.SelectMany(
        b => clist.Select(
            c => new { a, b, c }
        )
    )
);

foreach (var e in result)
{
    Console.Write("{0},", e);
}
{ a = 1, b = 3, c = 4 },{ a = 1, b = 3, c = 5 },{ a = 1, b = 4, c = 4 },{ a = 1, b = 4, c = 5 },{ a = 2, b = 3, c = 4 },{ a = 2, b = 3, c = 5 },{ a = 2, b = 4, c = 4 },{ a = 2, b = 4, c = 5 },

接受两个函数的SelectMany

还是先看例子:

int[] ilist = { 1, 2 };
double[] dlist = { 0.1, 0.2, 0.3 };

Func<int, double[]> collectionSelector =
    _ => dlist;
Func<int, double, string> resultSelector =
    (int_num, double_num) => String.Format("'{0}'", int_num + double_num);

var result = ilist.SelectMany(collectionSelector, resultSelector);
foreach (var e in result)
{
    Console.Write("{0},", e);
}

结果:

'1.1','1.2','1.3','2.1','2.2','2.3',

翻译规则

那么写出它的方法调用形式:

numQuery1 = numbers.SelectMany (
    _ => numbers,
    (num0, num1) => new {num0, num1}
).Where( x => x.num0 + x.num1 >11);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容