zoukankan      html  css  js  c++  java
  • Pro LINQ 之二:LINQ to Objects

    写在前面

    LINQ to Objects,是整个LINQ的支柱。而LINQ的语法,又存在类sql语言和扩展方法调用两种书写方式。从个人的习惯或者喜好出发,在LINQ to Objects上,我更倾向于使用后者。客观上,使用扩展方法的编译,也比类sql语言的方式更直接和效率。


    P54 LINQ to Objects的三大核心

    IEnumerable<T>、序列、标准查询运算符是LINQ to Objects的三大核心。

    其中,序列即数据源,需要支持IEnumerable接口。而序列的元素,则是序列的单位项,通常作为LINQ语句中形式参数的一种象征,出现在首个参数与返回值位置。扩展方法,或者说查询的运算符,是委托delegate的一种变形,这些方法均以序列的元素为首个参数,其本身则是构成LINQ子句的基本单位。

    对LINQ to Objects而言,查询运算的第一个参数与查询的结果,均为IEnumerable<T>类型。查询运算,如P14所言,大多均为迟延查询,即在查询结果被引用时,查询才真正被执行。所以,LINQ语句的错误,也通常要在查询结果被引用、遍历时才会被发现,因此不要认为LINQ语句通过了编译检查,就是bug-free的了。

    LINQ的迟延查询,可以用下面一段代码作为示意:

    // 准备数据源
    string[] strings = { "one", "two", null, "three" };
    
    // 定义LINQ查询
    Console.WriteLine("Before LINQ declared.");
    IEnumerable<string> ieStrings = strings.Where(s => s.Length == 3);
    Console.WriteLine("After LINQ declared.");
    
    // 执行查询,对序列进行遍历
    foreach (string s in ieStrings)
        Console.WriteLine("Processing  {0}", s);

    由于字符串数组strings中有一个元素为null,查询会触发一个空引用的异常。而我们要注意的是,这个异常是在foreach循环已经遍历并输出元素one与two后触发的,因此可以确定之前定义的LINQ查询,直到foreach循环引用序列中的元素中时才被执行。而另一方面,ToArray()、ToList()以及一些聚合操作,则是即时查询或生成的。

    P58 LINQ中的函数委托  Func<>

    LINQ中的标准查询运算符,大多都需要一个委托作为参数。这个委托,通常被表达为如下形式:

    public delegate TR Func<TR>();
    public delegate TR Func<T0, TR>(T0 a0);
    public delegate TR Func<T0, T1, TR>(T0 a0, T1 a1);
    public delegate TR Func<T0, T1, T2, TR>(T0 a0, T1 a1, T2 a2);
    public delegate TR Func<T0, T1, T2, T3, TR>(T0 a0, T1 a1, T2 a2, T3 a3);

    其中,T0通常代表作为数据源的输入序列,TR则代表作为结果的输出序列并且总在参数的最末尾。

    P59  标准查询运算符列表

    MSDN中的链接相比书中的更完整:标准查询运算符概述

    LINQ的查询符,涉及提取元素的多可带Func<T, bool>的判断子,涉及判断和比较元素的多可带IComparer<T>或IEqualityComparer<T>的比较子。其中IComparer是三元的(1/0/-1),IEqualityComparer是二元的(true/false)。

    Visual Studio的智能提示,对于我这样经常忘记运算符重载形式的人编写LINQ,帮助很大!


    迟延运算符

    注:运算符触发的异常,通常是因为元素为null。

    P65 Where

    重点是Where的第2种形式,其中引入了序列的索引值作为Func<>的参数。其中,p指代序列元素,i指代序列索引。

    //public static IEnumerable<T> Where<T>(
    //                            this IEnumerable<T> source,
    //                            Func<T, int, bool> predicate);
    IEnumerable<string> sequence = presidents.Where((p, i) => (i & 1) == 1);

    P68 Select

    同样有个引入了序列索引的、类似Where形式2的形式。

    var nameObjs = presidents.Select((p, i) => new { Index = i, LastName = p });

    Where与Select的结合:从字符串中找出所有Unicode编码的汉字。

    string source = @"我wo 是shi 中zhong 国guo 人ren";
    char[] result = source
        .Where(c => (c >= 0x4e00 && c <= 0x9fa5))
        .Select(c => c).ToArray();

    P73 SelectMany

    SelectMany的源是序列,源的元素也是序列,查询返回的结果是源的元素的元素。可以理解为,按1:M的规则,把序列中的每个元素再拆成多个子元素。它同样有个引入序列索引的形式:

    IEnumerable<char> chars = presidents
            .SelectMany((p, i) => i < 5 ? p.ToArray() : new char[] { });

    上述语句,会把字符串数组中的前5个字符串全部拆成单个字符放入查询结果chars中。

    P77 Take-TakeWhile-Skip-SkipWhile

    Take 从序列首部开始,取出固定数目的元素
    TakeWhile 从序列首部开始,取出符合条件的元素,直到遇到第一个条件不匹配的元素时停止,类似do...while
    Skip 从序列首部跳过N个元素
    SkipWhile 从序列首部,跳过符合条件的元素,直到遇到第一个不匹配的元素时停止。

    P83 Concat

    Concat合并两个序列,保存重复项(区别于Union)。连接2个以上的序列,有个小技巧

    new[]{sequence1, sequence2, …}.SelectMany()

    IEnumerable<string> items = new[] {
                                presidents.Take(5),
                                presidents.Skip(5)
                                }.SelectMany(s => s);

    P86 OrderBy-OrderByDescending-ThenBy-ThenByDescending-Reverse

    Order排序的难点在第2个形式,引入一个比较子IComparer<T>。

    public static IOrderedEnumerable<T> OrderBy<T, K>(
                                this IEnumerable<T> source,
                                Func<T, K> keySelector,
                                IComparer<K> comparer);

    在先前的.NET版本中,比较子通常是经由接口IComparable与IComparer实现的。在MSDN的知识库中有个例子:如何使用 IComparable 和 IComparer 接口在 Visual C# 中

    通过派生于IComparable变身比较器的容器,再内嵌IComparer的子类作为比较器,将可以通过Array.Sort(cars, car.sortYearAscending())这样的方式提供特定成员的比较器。

    另一方面,通过实现自IComparable派生来的接口方法Compare(),为Array.Sort(cars)这样的调用提供默认的比较器。

    public class car : IComparable
    {
        // 使用内嵌的IComparer派生类,构造特定成员的比较器
        private class sortYearAscendingHelper : System.Collections.IComparer
        {
            int System.Collections.IComparer.Compare(object a, object b)
            {
                car c1 = (car) a;
                car c2 = (car) b;
    
                if (c1.year > c2.year)
                    return 1;
                if (c1.year < c2.year)
                    return -1;
                else
                    return 0;
            }
        }
    
        // 利用嵌套类,暴露特定成员的比较器
        public static System.Collections.IComparer sortYearAscending()
        {
            return (System.Collections.IComparer) new sortYearAscendingHelper();
        }
    
        // 实现派生的IComparable.CompareTo()方法,提供类car默认的比较子
        int IComparable.CompareTo(object obj)
        {
            car c = (car) obj;
            if (this.year > c.year)
                return 1;
            if (this.year < c.year)
                return -1;
            else
                return 0;
        }
    
        private int year;
        public car(int Year)
        {
            year = Year;
        }
    }

    时代在进步,自.NET 2.0引入泛型之后,IComparer<T>接口进入我们的视野。通过从IComparer<T>派生子类,将直接生成对应于类型T的比较子,从而实现OrderBy((o=>o), new BoxComp())这样的排序。

    public class BoxComp : IComparer<Box>
    {
        public int Compare(Box x, Box y)
        {
            if (x.Height.CompareTo(y.Height) != 0)
            {
                return x.Height.CompareTo(y.Height);
            }
            else if (x.Length.CompareTo(y.Length) != 0)
            {
                return x.Length.CompareTo(y.Length);
            }
            else if (x.Width.CompareTo(y.Width) != 0)
            {
                return x.Width.CompareTo(y.Width);
            }
            else
            {
                return 0;
            }
        }
    }

    Reverse()就是简单地逆排序。

    P102 Join-GroupJoin

    Join的过程:用内选择子筛选内序列,用返回的对象借由外选择子筛选外序列,再按内外1:1的方式对匹配的内外元素调用结果的选择子生成结果序列中的元素。其中,外序列是主动的、必取的,在取出外元素的基础上,向结果选择子中添加匹配的内元素。结果选择子lambda表达式左边的参数是(outer, inner)。

    public static IEnumerable<V> Join<T, U, K, V>(
                        this IEnumerable<T> outer,                    // 外序列
                        IEnumerable<U> inner,                        // 内序列
                        Func<T, K> outerKeySelector,                // 外选择子
                        Func<U, K> innerKeySelector,                // 内选择子
                        Func<T, U, V> resultSelector);                // 结果选择子

    GroupJoin与Join的最大区别,在于GroupJoin是外内1:M的的匹配,即类似于数据库中父子表间通过外键FK建立的1:M的连接。其结果选择子Func<>中的第2个参数为IEnumerable<U>而不是U,因此选择子lambda表达式左边的参数表现为(outer, inners)。通常地,GroupJoin与inners.DefaultIfEmpty配合,可以得到左外连接的效果。

    var employeeOptions = employees
                            .GroupJoin(
                            empOptions,                // 内序列
                                e => e.id,                // 外选择子,其作用范围穿透至结果选择子内部
                                o => o.id,                // 内选择子,其作用范围无法穿透至结果选择子
                                (e, os) => os            // 结果选择子
                                    .DefaultIfEmpty()    // 如果没有匹配的内元素,即用Default替代
                                    // 用内结果序列IEnumerable<EmployeeOption>构造新的序列
                                    .Select(o => new    
                                        {
                                            id = e.id,
                                            name = e.firstName,
                                            //  内元素可能为null,故判断之
                                            options = o != null ? o.optionsCount : 0
                                        }))
                         // 得到序列IEnumerable<IEnumerable<匿名类>>,遂SelectMany拆解之
                            .SelectMany(r => r);

    P106 GroupBy

    经GroupBy()运算后,原IEnumerable<T>的序列将得到一个KeyValuePair<key, T>的序列。GroupBy()使用IEqualityComparer<>比较子,重载形式颇多。

    P112 Distinct-Union-Intersect-Except

    Distinct 剔除序列中的重复项
    Union 并集
    Intersect 交集
    Except 减集

    P117 Cast-OfType-AsEnumerable

    同前所述,多用OfType,少用Cast。这与C#的强制类型转换与as关键字或者TryParse()的区别是类似的。

    AsEnumerable是将支持IEnumerable<T>的序列变为IEnumerable<T>,从而屏蔽其原本自有的属性与方法(包括运算符)。比如A派生于IEnumerable<T>,有特型化的Where、Select等运算符,经AsEnumerable()后,便只提供IEnumerable<T>默认的运算符了。

    P128 Range-Repeat-Empty

    Range(int start, int count)

    产生一个从start开始,count个的整数序列

    Repeat<T>(int count)

    产生一个重复count次的T类型序列
    Empty<T>() 产生一个空的T类型序列

    非迟延运算符

    P136 ToArray-ToList-ToDictionary-ToLookup

    Dictionary要求Key唯一,而Lookup反之。都要求利用lambda表达式选择一个key,且支持自定义IComparer<T>。

    P148 SequenceEqual

    比较两个序列是否相同,另带一支持IEqualityComparer<T>比较子的格式。

    P151 First-Last-Single-ElementAt

    上述运算符,均有一个OrDefault版本以保证结果非null,另均可带一个返回bool类型的Func<T, bool>作为判断子。

    P164 Any-All-Contains

    Any:有否任意元素符合条件,可带一Func<T, bool>判断。

    All:所有元素是否符合条件,必须带一Func<T, bool>判断。

    Contains因其语义,可带一个IEqualityComparer<T>的比较子。

    P170 Count/LongCount-Min/Max-Sum/Average-Aggregate

    Aggregate用以实现自定义的聚合操作。对源序列中的每个元素调用一次Func<>,并且将聚合值作为Func<>的第一个参数。不同的重载形式,区别在于用作聚合初始值的值。之后再用Func<>返回值替代当前的聚合值。


    Pro LINQ 之三:LINQ to DataSet

  • 相关阅读:
    毕业了!
    mesos无执行器启动docker
    docker run option
    maintenance
    virtualenv
    multi role
    排序之插入排序
    DLU-1064 美声葱
    codeforces-1025 A Doggo Recoloring
    codeforces-1027 C Minimum Value Rectangle
  • 原文地址:https://www.cnblogs.com/Abbey/p/2104836.html
Copyright © 2011-2022 走看看