C#Linq

语言集成查询(Linq),是IEnumerable<T>接口提供了一组扩展方法,支持快捷的对序列进行操作,如过滤,映射,排序,分组,联合,求和,求平均数等。

Linq相关类型定义在System.Linq命名空间。

Linq的序列类型是IEnumerable<T>,也就是迭代器,迭代器在迭代时才获取数据。Linq中的许多方法返回也是IEnumerable<T>,因此也是在迭代是才返回数据。这些迭代时才返回数据的方法是延时方法。

Linq同时支持扩展方法调用和查询语法。查询语法是一种类似于SQL的语法,最终也是使用扩展方法。

过滤

通过提供一个判断条件,过滤数据源中的数据。

延时。


int[] source = new int[]{1,2,3,4,5};

var query = source.Where(x=>x%2==0);

var query = from n in source
    where n % 2 == 0
    select n;

foreach(var item in query)
{
    Console.WriteLine(item);
}

如果仅仅需要过滤,使用扩展方法更简洁。

类型过滤

返回数据源中符合类型的数据。

延时。


object[] source = new object[]{1,2,3,"4",5};

var query = source.OfType<int>();

通过该方法,还可以将非泛型的枚举器转换成泛型的,后续可以进行更多操作。

映射

将序列中的每个元素映射成另一个。

延时。


int[] source = new int[]{1,2,3,4,5};

var query = source.Select(x=>x*10);

var query = from n in source
            select n*10;

foreach(var item in query)
{
    Console.WriteLine(item);
}

如果仅仅需要映射,使用扩展方法更简洁。

映射多个

将数据源中的每个元素的某个可枚举成员映射成另一个数据源。

延时。


int[] source = new int[]{1,2,3,4,5};

var query = source.SelectMany(x=>source,(x,y)=>new{x,y});

var query = from x in source
            from y in source
            select new{x,y};

上述方法在返回时返回了自身,形成的结果是自身的交叉连接。

排序

根据某个键进行排序。

延时。


string[] source = new string[]{"ok","hello","hi","world"};

var query = source
    .OrderBy(x=>x.Length)
    .ThenByDescending(x=>x);

var query = from n in source
            orderby n.Length,n descending
            select n;

foreach(var item in query)
{
    Console.WriteLine(item);
}

在进行多次排序时,使用查询语法更简洁。

分组

将数据源按照指定的键进行分组。分组的结果类型是IGrouping<TKey,TElement>,包含一个键和分组结果。

延时。

分组结果继承自IEnumerable<TElement>,包含一个额外的Key属性。

public interface IGrouping<out TKey, out TElement> : IEnumerable<TElement>, IEnumerable
{
    TKey Key { get; }
}


string[] source = new string[]{"ok","hello","hi","world"};

var query = source
    .Groupby(x=>x.Length);

var query = from n in source
    group by n.Length
    select n;

foreach(var item in query)
{
    Console.WriteLine(item.Key);
    Console.WriteLine(string.Join(", ",item);
}

去重

去重序列的重复元素。

延时。

int[] source = new int[]{1,2,3,4,3,2};

var query = source.Distinct();

反转

反转集合的元素。

延时。

int[] source = new int[]{1,2,3,4,5};

var query = source.Reverse();

连接

将一个数据源和另一个进行内连接。

class Product
{
    public int Id { get; set; }
    public int CategoryId { get; set; }
    public string Name { get; set; }
}

class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
}

内连接需要提供两个集合的键选择器,相等的键返回选择的结果。

延迟。


var products = new List<Product> {
    new Product{Id=1,CategoryId=1,Name="产品1" },
    new Product{Id=2,CategoryId=1,Name="产品2" },
    new Product{Id=3,CategoryId=2,Name="产品3" }
};

var categories = new List<Category> {
    new Category{Id = 1,Name="分类1"},
    new Category{Id = 2,Name="分类2"},
};


var query = products.Join(categories,
    p => p.CategoryId,
    c => c.Id,
    (p, c) => new { p, c });

var query = from p in products
            join c in categories
            on p.CategoryId equals c.Id
            select new { p, c };

在进行连接时,使用查询语法更简洁。

分组连接

分组连接可以将右边的连接以列表作为结果。

该方法的结果是延迟的。

var products = new List<Product> {
    new Product{Id=1,CategoryId=1,Name="产品1" },
    new Product{Id=2,CategoryId=1,Name="产品2" },
    new Product{Id=3,CategoryId=2,Name="产品3" }
};

var categories = new List<Category> {
    new Category{Id = 1,Name="分类1"},
    new Category{Id = 1,Name="分类3"},
    new Category{Id = 2,Name="分类2"},
};


var query = products
    .GroupJoin(categories,
        p => p.CategoryId, c => c.Id,
        (p, cs) => new { p, cs })
    .SelectMany(temp => temp.cs, (a, c2) => new { a.p, c2 });;

var query = from p in products
                join c in categories into cs
                on p.CategoryId equals c.Id
                from c2 in cs
                select new { p, c2 };

GroupJoin选择器的第二个参数cs是一个集合。

在进行分组连接时,查询语法更加简洁。

左连接

左连接需要保证左边的完整性,即在右边没有值是提供默认值。

DefautIfEmpty可以在序列为空时提供一个空元素。

 var products = new List<Product> {
    new Product{Id=1,CategoryId=1,Name="产品1" },
    new Product{Id=2,CategoryId=1,Name="产品2" },
    new Product{Id=3,CategoryId=2,Name="产品3" },
    new Product{Id=4,CategoryId=0,Name="产品4" }

};

var categories = new List<Category> {
        new Category{Id = 1,Name="分类1"},
        new Category{Id = 1,Name="分类3"},
        new Category{Id = 2,Name="分类2"},
    };


var query = products
    .GroupJoin(categories,
        p => p.CategoryId, c => c.Id,
        (p, cs) => new { p, cs = cs.DefaultIfEmpty() })
    .SelectMany(temp => temp.cs, (a, c2) => new { a.p, c2 });

var query2 = from p in products
                join c in categories
                on p.CategoryId equals c.Id into cs
                from c2 in cs.DefaultIfEmpty()
                select new { p, c2 };

右连接将两个数据源交换即可。

总数

获取数据源的总数量。

此方法是立即执行的。


string[] source = new string[]{"ok","hello","hi","world"};

var count = source.Count();

对于数组,列表,可以直接获取结果,对于其他情况,可能需要枚举一次所有元素。

聚合函数

求数据源的指定键的和、平均数、最大值、最小值。

这些方法是立刻执行的。


int[] source = new int[]{1,2,3,4,5};

var sum = source.Sum();
var max = source.Max();
var min = source.Min();
var avg = source.Average();

对数据源的数据进行聚合操作,根据指定的聚合函数。

此方法是立即执行的。

int[] source = new int[]{1,2,3,4,5};

//计算所有元素的和
source.Aggregate((x, y) => x * y);

集合关系

求数据源和另一个数据源的连接,并集,交集,差集。

这些方法是延时执行的。

int[] source = new int[]{1,2,3,4,5};
int[] second = new int[]{2,7,9};

var concat = source.Concat(second);//连接两个数据源,1,2,3,4,5,2,7,9
var union = source.Union(second);//连接两个数据源,并去重,1,2,3,4,5,7,9
var intersect = source.Intersect(second);//两个数据源都有的,2
var except = source.Except(second);//第一个数据源有而第二个没有的,1,3,4,5

取特定元素

获得特定位置的元素,头部元素,尾部元素。

这些方法是立即执行的。


int[] source = new int[]{1,2,3,4,5};

int at = source.ElementAt(3);
int first = source.First();
int last = soource.Last();

可以使用对应的带Default结尾的方法在没有获取到值时提供默认值。

如果数据源不是数组和列表,ElementAtLast方法效率很低,需要先读取前面的元素。

判断

判断数据源是否包含特定元素

此方法是立即执行的。


int[] source = new int[]{1,2,3,4,5};

bool contains = source.Contains(10);

判断数据源是否存在符合条件的元素

如果没有任何条件,判断数据源是否有元素,如果没有任何元素,返回false

此方法是立即执行的。


int[] source = new int[]{1,2,3,4,5};

bool contains = source.Any(x=>x==10);

bool hasElement = source.Any();

通过此方法可以高效判断序列是否有元素。

判断数据源是否所有元素都符合条件

数据源如果没有元素,则返回true

此方法是立即执行的。


int[] source = new int[]{1,2,3,4,5};

bool contains = source.All(x=>x<10);

判断两个集合是否按照顺序一一对应相等

int[] source = new int[]{1,2,3,4,5};
int[] second = new int[]{,1,2,3,5};

bool equal = source.SequenceEqual(second);

跳过和限定

这些方法是延迟的。

跳过特定条数


int[] source = new int[]{1,2,3,4,5};

var query = source.Skip(3);

跳过特定条件元素


int[] source = new int[]{1,2,3,4,5};

var query = source.SkipWhile(x=>x<=3);

取特定数量元素


int[] source = new int[]{1,2,3,4,5};

var query = source.Take(10);

取特定条件元素


int[] source = new int[]{1,2,3,4,5};

var query = source.TakeWhile(x=>x<10);

合并序列

将两个序列的对应位置元素映射为新的元素。

如果元素数量不一致,以元素数小的为准。

该方法是延迟的。

int[] source = new int[]{1,2,3,4,5};
int[] second = new int[]{5,4,3,2};

var query = source.Zip(second,(x,y)=>new{x,y});

转换

转换为数组

该方法是立刻执行的。


int[] source = new int[]{1,2,3,4,5};

var array = source.ToArray();


转换为列表

该方法是立刻执行的。


int[] source = new int[]{1,2,3,4,5};

var list = source.ToList();


转换为字典

根据键选择器和值选择器自序列获取字典。键要求不能重复。

该方法是立刻执行的。


int[] source = new int[]{1,2,3,4,5};

var dict = source.ToDictionary(x=>x,y=>y);

强制转换

将序列的元素强制转换为特定类型元素序列

如果有元素不符合条件,会抛异常。

该方法是延时的。


int[] source = new int[]{1,2,3,4,5};

var query = source.CastTo<int>();

透明标识符

在查询语句中,可以使用let保存查询过程中的临时变量。


string[] source = new string[]{"ok","hello","hi","world"};

var query = from n in source
            let length = n.Length
            orderby length
            select n;

透明标识符可以保存一些查询中的中间变量。如果某个操作很费时,就适合使用透明标识符来预先保存。

在内部,透明标识符使用了匿名类型。


string[] source = new string[]{"ok","hello","hi","world"};

var query = source
    .Select(x=>new{value=x,length = x.Length})
    .OrderBy(temp=>temp.length)
    .Select(temp=>temp.value);

在需要保存中间变量的情况,使用查询语法的透明标识符更加简洁。

方法和查询语法的选择

方法可以实现所有功能,查询语法仅仅支持部分。

在排序时,使用查询语法更简洁。

在连接时,使用查询语法更简洁。

在需要中间变量时,使用查询语法更简洁。

注意事项

Linq的很多查询是延时的,这意味着每次使用结果都重新查询了结果,如果要重复使用同一结果,一对要先执行非延时方法保持结果,如ToList

Linq简化了代码,但复杂了调试。

并行Linq

并行Linq(PLinq)是指运用多核执行Linq的操作。

并行Linq相关的方法定义在ParallelEnumerable类型。

转换为并行Linq

IEnumerable<T>的扩展方法AsParallel可以将序列转换成并行Linq。

并行Linq的方法和Linq是一致的,只是多了许多设置。


var source = new int[]{1,2,3,4,5};

var query = source.AsParallel().ToList();

需要注意的是,如果数据量本身很小,那么使用并行Linq将得不偿失,因为并行自身也要付出代价。

AsSequential可以将并行Linq转换回顺序计算。

并行度

通过WithDegreeOfParallelism方法,可以设置使用的核心数,默认是当前CPU核心数。

通常,这个设置不需要更改。


var source = new int[]{1,2,3,4,5};

var query = source
    .AsParallel()
    .WithDegreeOfParallelism(4)
    .ToList();

有序和无序

并行操作的时候,有多个线程进行操作,如果不设置,结果顺序会和源不一致。

如果要维持结果的顺序,需要调用AsOrdered方法。

var source = new int[]{1,2,3,4,5};

var query = source
    .AsParallel()
    .AsOrdered()
    .ToList();

可以调用AsUnordered忽略顺序。

Linq To XML

Systme.Xml.Linq命名空间提供类型,用于使用Linq来访问Xml文档。

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<store>
    <books>
        <book>
            <category>reference</category>
            <author>Nigel Rees</author>
            <title>Sayings of the Century</title>
            <price>8.95</price>
        </book>
          <book>
            <category>fiction</category>
            <author>Evelyn Waugh</author>
            <title>Sword of Honour</title>
            <price>12.99</price>
        </book>
    </books>
</store>

 var doc = XDocument.Load(new StreamReader("book.xml"));


foreach (var element in doc.Descendants("book"))
{
    Console.WriteLine(element.Element("category")?.Value);
    Console.WriteLine(element.Element("author")?.Value);
    Console.WriteLine(element.Element("title")?.Value);
    Console.WriteLine(element.Element("price")?.Value);
}

相关类型:

XElement表示Xml文档的元素。

方法:

public IEnumerable<XElement> Ancestors(XName name)获取节点的所有特定名称的祖先节点。

public IEnumerable<XElement> Descendants(XName name)获取节点的特定名称的所有后代节点。

public XElement Element(XName name)获取节点的首个特定名称子节点。

public IEnumerable<XElement> Elements(XName name);获取节点的特定名称子节点。

IQueryable

System.Linq命名空间包含类型IQueryable<T>,用来表示向任意数据源的查询。

System.Linq.Queryable<T>提供了和System.Linq.Enumerable<T>一致的扩展方法,但是其委托参数是对应的System.Linq.Expressions.Expression<T>类型。

Expression<T>类型表示一个特定类型的Lambda表达式树,其构造方式和Lambda表达式创建的委托相似,但是其包含了代码的数据。

Lambda表达式树有许多限制,比如只能有单行代码。

Expression<Func<int,int,int>> add = (x,y)=>x+y;

Func<int,int,int> func = add.Compile();

Console.WriteLine(func(100,200));

上面的代码等价于:

ParameterExpression left = Expression.Parameter(typeof(int), "x");
ParameterExpression right = Expression.Parameter(typeof(int), "y");

Expression body = Expression.Add(left, right);

Expression<Func<int, int, int>> add = Expression.Lambda<Func<int, int, int>>(body, left, right);

Func<int, int, int> func = add.Compile();

Console.WriteLine(func(100, 200));

C#提供了Lambda表达式的语法来创建表达式树,表达式树和一般Labmda表达式有很大不同,它包含了代码的结构,而一般Lambda只是一个委托(函数指针),不包含数据。

表达式树可以编译成一般委托,但反之不行。


    public interface IQueryable : IEnumerable
    {
        //获取与实例相关联的表达式目录树。
        Expression Expression { get; }

        //获取与此实例关联的表达式树时返回的元素的类型。
        Type ElementType { get; }

        //获取与此数据源相关联的查询提供程序。
        IQueryProvider Provider { get; }
    }

    public interface IQueryable<out T>: IQueryable
    {

    }

    public interface IQueryProvider
    {
        IQueryable CreateQuery(Expression expression);

        IQueryable<TElement> CreateQuery<TElement>(Expression expression);

        object Execute(Expression expression);

        TResult Execute<TResult>(Expression expression);
    }

IQueryable容纳了一个查询的表达式树,通过解析这个树,可以向其他地方进行查询。

为了和一般的Linq进行区别,一般将IEnumerable<T>上的Linq称作Linq To Objects。

Queryable提供了AsQueryable扩展方法,可以将IEnumerable<T>转换成IQueryable<T>。 这个IQueryable<T>ProviderEnumerableQuery<T>只是将其表达式编译成一般委托,其查询表现和IEnumerable<T>一致。

Entity Framework

Entity Framework,简称EF,是一个通过Linq进行数据库访问的对象关系映射(ORM)框架,可以将查询表达式树翻译成SQL

MSSQL创建表SQL

create table Product(
    Id int primary key,
    Name varchar(200) not null
)

类型定义


using EntityFramework;
using System.ComponentModel.DataAnnotations;

[Table("Product")]
public class Product
{
    [Key]
    public int Id {get;set;}
    [Required]
    public string Name{get;set;}
}

public class MyDbContext:DbContext
{
    public MyDbContext():base("name=ConnectionString")

    DbSet<Product> Product{get;set;}
}

其中DbSet<T>类型继承自IQueryable<T>

static void Main(string[] args){

    using(var db = new MyDbContext())
    {
        var query = from n in db.Product
                    where n.Name.Contains("中")
                    orderby n.Name descending,n.Id;

        foreach(var product in query){
            Console.WriteLine(product.Id);
            Console.WriteLine(product.Name);
        }
    }
}

代码生成的SQL

select Id,Name
from Product
where Name like '%中%'
order by Name desc,Id asc

Where方法的条件表达式树翻译成SQL的where语句。 将OrderBy,OrderByDescending,ThenBy,ThenByDescending的表达式树翻译成SQL的order by语句。 Select方法的选择表达式树选择的翻译成SQL的select语句。 将Product类的属性翻译成对应的表字段。 将数据源的类型Product映射到表Product

需要谨记的是,此查询是被翻译到SQL的,因此:

始终清楚查询在何处执行。

IQueryable<T>上执行查询立即函数会导致查询,如ToList。如果先ToList再过滤,过滤则是在内存进行操作。

static void Main(string[] args){

    using(var db = new MyDbContext())
    {
        var query = from n in db.Product.ToList()
                    where n.Name.Contains("中")
                    order by n.Name descending,n.Id;

        foreach(var product in query){
            Console.WriteLine(product.Id);
            Console.WriteLine(product.Name);
        }
    }
}

该查询查询了表的所有数据,并在内存中进行了过滤和排序,这对于大表是不可取的。