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

posted @ 2011-07-14 02:22  没头脑的老毕  阅读(1063)  评论(3编辑  收藏  举报