C# LINQ

LINQ(Language Integrated Query,语言集成查询)。在C# 语言中集成了查询语法,可以用相同的语法访问不同的数据源。

命名空间System.Linq下的类Enumerate中定义了许多LINQ扩展方法,用于可以在实现了IEnumerable<T>接口的任意集合上使用LINQ查询。

扩展方法

C#扩展方法在静态类中声明,定义为一个静态方法,其中第一个参数定义了它扩展的类型,扩展方法必须对第一个参数使用this关键字。

public static class StringExtension
{
public static void WriteLine(this string str)
{
Console.WriteLine(str);
}
}

调用方式有两种:

//方式一
"测试".WriteLine();
//方式二
StringExtension.WriteLine("测试二");

采用方式一的方式调用,需要导入该扩展方法所在类的命名空间即可。在使用LINQ时,需要导入System.Linq命名空间。

示例实体定义

为了更好的说明LINQ的使用, 我们将使用具体的示例进行说明,在该示例中,分别定义如下几个实体:

Racer.cs:该类用来显示赛车手信息

public class Racer : IComparable<Racer>, IFormattable
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Wins { get; set; }
public string Country { get; set; }
public int Starts { get; set; }
public IEnumerable<string> Cars { get; }
public IEnumerable<int> Years { get; } public Racer(string firstName, string lastName, string country,
int starts, int wins, IEnumerable<int> years, IEnumerable<string> cars)
{
this.FirstName = firstName;
this.LastName = lastName;
this.Country = country;
this.Starts = starts;
this.Wins = wins;
this.Years = years != null ? new List<int>(years) : new List<int>();
this.Cars = cars != null ? new List<string>(cars) : new List<string>();
} public Racer(string firstName, string lastName, string country, int starts, int wins)
: this(firstName, lastName, country, starts, wins, null, null)
{
} public override string ToString() => FirstName + " " + LastName; public int CompareTo(Racer other) => LastName.CompareTo(other?.LastName); public string Tostring(string format) => ToString(format, null); public string ToString(string format, IFormatProvider formatProvider)
{
switch (format)
{
case null:
case "N":
return "None";
case "F":
return FirstName;
case "L":
return LastName;
case "C":
return Country;
case "S":
return Starts.ToString();
case "W":
return Wins.ToString();
case "A":
return $"{FirstName} {LastName},{Country}; start:{Starts}, wins:{Wins}";
default:
throw new FormatException($"Format {format} not supproted");
}
}
}

Team.cs:该类包含获得冠军车队称号的车队名称和年份

public class Team
{
public string Name { get; }
public IEnumerable<int> Years { get; } public Team(string name, params int[] years)
{
this.Name = name;
this.Years = years != null ? new List<int>(years) : new List<int>();
}
}

Formulal.cs:返回获的冠军的赛车手信息集合、冠军车队列表。

public static class Formulal
{
private static List<Racer> _racers;
public static IList<Racer> GetChampions()
{
if (_racers == null)
{
_racers = new List<Racer>(40);
_racers.Add(new Racer("Nino", "Farina", "Italy", 33, 5
, new int[] { 1950 }, new string[] { "Alfa Rmomeo" }));
_racers.Add(new Racer("Alberto", "Ascari", "Italy", 32, 10
, new int[] { 1952, 1953 } , new string[] { "Ferrari" }));
_racers.Add(new Racer("Juan Manuel", "fangio", "Argentina", 51, 24
, new int[] { 1951, 1954, 1955, 1956, 1957 }
, new string[] { "Alfa Rmomeo", "Maserati", "Mercedes", "Ferrari" }));
_racers.Add(new Racer("MIke", "Hawthorn", "UK", 45, 3
, new int[] { 1958 }, new string[] { "Ferrari" }));
_racers.Add(new Racer("Phil", "Hill", "USA", 48, 3
, new int[] { 1961 }, new string[] { "Ferrari" }));
_racers.Add(new Racer("John", "Surtees", "UK", 111, 6
, new int[] { 1964 }, new string[] { "Ferrari" }));
_racers.Add(new Racer("Jim", "Clark", "UK", 72, 25
, new int[] { 1963, 1965 }, new string[] { "Lotus" }));
_racers.Add(new Racer("Jack", "Brabham", "Australia", 125, 14
, new int[] { 1959, 1960, 1966 }
, new string[] { "Cooper", "Brabham" }));
_racers.Add(new Racer("Denny", "Hulme", "New Zealand", 112, 8
, new int[] { 1967 }, new string[] { "Brabham" }));
_racers.Add(new Racer("Graham","Hill","UK",176,14,newint[]{1962,1968},newstring[]{"BRM","Lotus"}));
_racers.Add(newRacer("Jochen","Rindt","Austria",60,6,newint[]{1970},newstring[]{"Lotus"}));
_racers.Add(newRacer("Jackie","Stewart","UK",99,27,newint[]{1969,1971,1973},newstring[]{"Matra","Tyrrell"}));
_racers.Add(newRacer("张","小新","China",86,6,newint[]{1974},newstring[]{"Brabham"}));
_racers.Add(newRacer("刘","备","China",98,15,newint[]{1976,1977},newstring[]{"Brabham","Lotus"}));
_racers.Add(newRacer("关","羽","China",130,14,newint[]{1975,1979,1981},newstring[]{"Tyrrell"}));
_racers.Add(newRacer("曹","操","China",89,18,newint[]{1978},newstring[]{"Cooper","BRM"}));
_racers.Add(newRacer("赵","云","China",83,11,newint[]{1980,1983},newstring[]{"BRM","Lotus"}));
_racers.Add(newRacer("刘","邦","China",108,16,newint[]{1982},newstring[]{"Brabham","Matra","Lotus"}));
_racers.Add(newRacer("项","羽","China",100,26,newint[]{1984,1985,1986},newstring[]{"Ferrari"}));}return _racers;}privatestaticList<Team> _teams;publicstaticIList<Team>GetContructorChampions(){if(_teams ==null){
_teams =newList<Team>(){newTeam("Vanwall",1958),newTeam("Cooper",1959,1960),newTeam("Ferrari",1961,1964,1975,1976,1977,1979,1982,1983,1999,2000,2001,2002,2003,2004,2007,2008),newTeam("BRM",1962),newTeam("Lotus",1963,1965,1968,1970,1972,1973,1978),newTeam("Brabham",1966,1967),newTeam("Matra",1969),newTeam("Tyrrell",1971),newTeam("McLaren",1974,1984,1985,1988,1989,1990,1991,1998),newTeam("Williams",1980,1981,1992,1993,1994,1996,1997),newTeam("Benetton",1995),newTeam("Renault",2005,2006),newTeam("Brawn GP",2009),newTeam("三国",1974,1975,1976,1977,1978,1979,1980,1981,1982,1983),newTeam("汉强",1984,1985,1986,1987)};}return _teams;}privatestaticList<Championship> championships;publicstaticIEnumerable<Championship>GetChampionships(){if(championships ==null){
championships =newList<Championship>();
championships.Add(newChampionship{Year=1950,First="Nino Farina",Second="Juan Manuel Fangio",Third="Luigi Fagioli"});
championships.Add(newChampionship{Year=1951,First="Juan Manuel Fangio",Second="Alberto Ascari",Third="Froilan Gonzalez"});}return championships;}}

Championship.cs 和RacerInfo.cs:附加类

public class Championship
{
public int Year { get; set; }
public string First { get; set; }
public string Second { get; set; }
public string Third { get; set; }
}
public class RacerInfo
{
public int Year { get; set; }
public int Positon { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}

LINQ查询

例如,查询来自China的世界冠军,并按照夺冠次数排序:

var query = from r in Formulal.GetChampions()
where r.Country == "China"
orderby r.Wins descending
select r; foreach (Racer r in query)
{
Console.WriteLine($"{r:A}");
}

上述中的query之后的语句就是一个LINQ查询表达式。查询表达式必须以from子句开头,以selectgroup子句结束。在这两个子句之间,可以使用whereorderbyjoinlet和其他from子句。

在编译时,查询表达式根据 C# 规范规则转换成标准查询运算符方法调用。 可使用查询语法表示的任何查询都可以使用方法语法进行表示。 不过,在大多数情况下,查询语法的可读性更高,也更为简洁。

注意:上述示例中的变量query只指定了LINQ查询,该查询不是通过这个赋值语句执行的,而是在使用foreach语句对其进行循环访问时,该查询才会执行,关于这一点,请查看下述中”推迟查询的执行“的相关说明。

将上述的查询表达式使用LINQ扩展方法进行实现:

var champions = new List<Racer>(Formulal.GetChampions());
IEnumerable<Racer> brazilChampions = champions.Where(r => r.Country == "China")
.OrderByDescending(r => r.Wins)
.Select(r => r); foreach (Racer r in brazilChampions)
{
Console.WriteLine($"{r:A}");
}

推迟查询的执行

在运行期间定义查询表达式时,查询并不会运行,而是在迭代数据项是运行。例如:

var names = new List<string> { "Nino", "Alberto", "Juan", "Mike", "Phil" };
//在定义的时候并不会马上执行
var namesWithJ = from n in names
where n.StartsWith("J")
orderby n
select n;
Console.WriteLine("以J开头的有:");
//在遍历的时候才出发表达式的执行
foreach (string name in namesWithJ)
{
Console.WriteLine(name);
} names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
Console.WriteLine("添加元素后:");
//再次出发表达式的执行
foreach (string name in namesWithJ)
{
Console.WriteLine(name);
}

上述执行结果如下:

以J开头的有:
Juan
添加元素后:
Jack
Jim
John
Juan
>

通过这个示例可以很清楚的说明,在定义LINQ表达式赋值语句时,并没有马上得到结果,而是在需要遍历时,才将结果返回。这就是为什么在第二次添加元素后 ,不用再重新使用赋值语句,就可以直接遍历得到结果。我们把这种不会立即反馈结果的情况,叫做 延迟加载或推迟查询。

解决延迟加载的办法就是使用ToArray()ToList()等方法,它们可以在定义好查询表达式后立即返回查询的结果。例如:

var names = new List<string> { "Nino", "Alberto", "Juan", "Mike", "Phil" };
var namesWithJToList = (from n in names
where n.StartsWith("J")
orderby n
select n).ToList(); //注意:此处使用了ToList()方法 Console.WriteLine("调用ToList():");
foreach (string name in namesWithJToList)
{
Console.WriteLine(name);
}
names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
Console.WriteLine("添加了新的元素后:");
foreach (string name in namesWithJToList)
{
Console.WriteLine(name);
}

上述执行结果:

调用ToList():
Juan
添加了新的元素后:
Juan
>

可以很清楚的看到,在调用ToList()方法之后,LINQ表达式会立即执行,此时变量namesWithJToList存储的就是查询得到的结果集,它是一个独立的List集合,当再次添加元素时,该集合中的结果并没有发生改变,遍历时,依旧得到的是第一次的结果。

标准的查询操作符

标准查询操作符 说明
Where OfType<TResult> 筛选操作符定义了返回元素的条件。在Where查询操作符中可以使用谓词,例如,lambda表达式定义的谓词,来返回布尔值。OfType<IResult>根据类型筛选元素,只返回TResult类型的元素
Select SelectMany 投射操作符用于把对象转换为另一个类型的新对象。SelectSelectMany定义了根据选择函数选择结果值的投射
OrderBy ThenBy OrderByDescending ThenByDescending Reverse 排序操作符改变所返回的元素的顺序。OrderBy按升序排序,OrderByDescending按降序排序。如果第一次排序的结果很类似,就可以使用ThenByThenBy Descending操作符进行第二次排序。Reverse反转集合中元素的顺序。
Join GroupJoin 连续操作符用于合并不直接相关的集合。使用Join操作符,可以根据键选择器函数联接两个集合,这类似于SQL中的JOINGroupJoin操作符联接两个集合,组合其结果。
GroupBy ToLookup 组合操作符把数据放在组中。GroupBy操作符组合有公共键的元素。ToLookup通过创建一个一对多字典,来组合元素。
Any All Contains 如果元素序列满足指定的条件,限定符操作符就返回布尔值。AnyAllContains都是限定符操作符。Any确定集合中是否有满足谓词函数的元素;All确定集合中的所有元素是否都满足谓词函数;Contains检查某个元素是否在集合中。
Take Skip TakeWhile SkipWhile 分区操作符返回集合的一个子集。TakeSkipTakeWhileSkipWhile都是分区操作符。使用它们可以得到部分结果。使用Take必须指定要从集合中提取的元素的个数;Skip跳过指定的元素个数,提取其他元素;TakeWhile提取条件为真的元素,SkipWhile跳过条件为真的元素。
Distinct Union Intersect Except Zip 这些操作符都被称为Set操作符,因为它们最终返回一个集合。Distinct从集合中删除重复的元素。除了Distinct之外,其他Set操作符都需要两个集合。Union返回出现在其中一个集合中的唯一元素。Intersect返回两个集合中都有的元素。Except返回只出现在一个集合中的元素。Zip把两个集合合并为一个。
First FirstOrDefault Last LastOrDefault ElementAt ElementAtDefault Single SingleOrDefault 这些元素操作符仅返回一个元素。First返回第一个满足条件的元素。FirstOrDefault类似于First,但如果没有找到满足条件的元素,就返回类型的默认值。Last返回最后一个满足条件的元素。ElementAt指定了要返回的元素的位置。Single只返回一个满足条件的元素。如果有多个元素都满足条件,就抛出一个异常。所有的XXXOrDefault方法都类似于以相同前缀开头的方法,但如果没有找到该元素,他们就返回类型的默认值。
Count Sum Min Max Average Aggregate 聚合操作符计算集合的一个值。利用这些聚合操作符,可以计算所有值的总和。所有元素的个数、值最大和最小的元素,以及平均值等。
ToArray AsEnumerable ToList ToDictionary Cast<TResult> 这些转换操作符将集合转换为数组:IEnumerableIListIDictionary等。Cast方法把集合的每个元素类型转换为泛型参数类型。
Empty Range Repeat 这些生成操作符返回一个新集合。使用Empty时集合时空的;Range返回一系列数字;Repeat返回一个始终重复一个值的集合。

筛选(Where)

LINQ中使用Where对结果集进行过滤和筛选。在使用LINQ查询表达式时,where子句 可以合并多个表达式。

var racrers = from r in Formulal.GetChampions()
where r.Wins > 15 && (r.Country == "China" || r.Country == "UK")
select r;

上述使用LINQ扩展方法形式:

var racres2 = Formulal.GetChampions()
.Where(r => r.Wins > 15 && (r.Country == "China" || r.Country == "UK"))
.Select(r => r);

这两种执行的结果都是一样的。但是并不是所有的LINQ查询都可以使用LINQ查询表达式完成,也不是所有的LINQ扩展方法都能够映射到LINQ查询表达式语句上。LINQ扩展方法使用的范围要比查询表达式更广泛,尤其是一些高级查询,只能或更多的是使用扩展方法完成。同时,这两种形式也可以组合使用。

例如,使用Where()的另一种重载方法用索引进行筛选:

//使用索引筛选,只能使用扩展方法,不能使用Linq查询语句
var racers3 = Formulal.GetChampions()
.Where((r, index) => r.LastName.StartsWith("A") && index % 2 != 0);

关于查询表达式和查询语法应该如何选择,这里引用了官方说明:

在编译时,查询表达式根据 C# 规范规则转换成标准查询运算符方法调用。 可使用查询语法表示的任何查询都可以使用方法语法进行表示。 不过,在大多数情况下,查询语法的可读性更高,也更为简洁。

通常,我们建议在编写 LINQ 查询时尽量使用查询语法,并在必要时尽可能使用方法语法。 这两种不同的形式在语义或性能上毫无差异。 查询表达式通常比使用方法语法编写的等同表达式更具可读性。

一些查询操作(如 Count 或 Max)没有等效的查询表达式子句,因此必须表示为方法调用。 可以各种方式结合使用方法语法和查询语法。 有关详细信息,请参阅 LINQ 中的查询语法和方法语法

本段文字原文链接:https://docs.microsoft.com/zh-cn/dotnet/csharp/linq/index

类型筛选(OfType)

可以使用OfType()扩展方法基于类型进行筛选。

object[] data = { "one", 2, 3, "four", "five", 6 };
var query = data.OfType<string>();
foreach (var s in query)
{
Console.WriteLine(s);
}

复合的from子句(SelectMany)

如果一个集合中的某个对象依旧是一个集合,就可以使用from子句进行筛选。

//Formulal.GetChampions()返回Racer集合,每一个Racer的属性Cars是一个字符串数组
var ferrariDrivers = from r in Formulal.GetChampions()
from c in r.Cars
where c == "Ferrari"
orderby r.LastName
select r.FirstName + " " + r.LastName;

C#编译器把复合的from子句和LINQ查询转换为SelectMany()扩展方法。SelectMany()方法可以迭代序列的序列。

var ferrariDrivers2 = Formulal.GetChampions()
.SelectMany(r => r.Cars, (r, c) => new { Racer = r, Car = c })
.Where(r => r.Car == "Ferrari")
.OrderBy(r => r.Racer.LastName)
.Select(r => r.Racer.FirstName + " " + r.Racer.LastName);

排序(OrderBy/OrderByDescending/ThenBy/ThenByDescending)

var racers3 = (from r in Formulal.GetChampions()
orderby r.Country, r.LastName, r.FirstName ascending
select r).Take(10);

使用扩展方法:

var racers4 = Formulal.GetChampions()
.OrderBy(r => r.Country)
.ThenBy(r => r.LastName)
.ThenByDescending(r => r.FirstName)
.Take(10);

分组(GroupBy)

如果要根据一个关键字值 对查询结果分组,可以使用group子句。对应的扩展方法为GroupBy()

var countries = from r in Formulal.GetChampions()
group r by r.Country into g //将分组信息放入标识符g中
orderby g.Count() descending, g.Key
where g.Count() >= 2
select new
{
Country = g.Key,
Count = g.Count()
};

使用扩展方法的形式:

var countries2 = Formulal.GetChampions()
.GroupBy(r => r.Country)
.OrderByDescending(g => g.Count())
.ThenBy(g => g.Key)
.Where(g => g.Count() >= 2)
.Select(g => new { Country = g.Key, Count = g.Count() });

在LINQ查询语句中使用变量

上述的分组查询语句,中间多次调用了Count方法,使用let子句可以改变这种方式。let允许在LINQ查询中定义变量。

var countries = from r in Formulal.GetChampions()
group r by r.Country into g
let count = g.Count()
orderby count descending, g.Key
where count >= 2
select new
{
Country = g.Key,
Count = count
};

使用方法语法时,为了避免Count方法被调用多次,可以使用Select方法创建一个匿名类型,将Count方法的结果作为匿名类的属性进行传递:

var countries2 = Formulal.GetChampions()
.GroupBy(r => r.Country)
.Select(g => new { Group = g, Count = g.Count() })
.OrderByDescending(g => g.Count)
.ThenBy(g => g.Group.Key)
.Where(g => g.Count >= 2)
.Select(g => new
{
Country = g.Group.Key,
Count = g.Count
});

注意:应考虑根据let子句或Select方法创建的临时对象的数量 。查询大列表时,创建的大量对象需要以后进行垃圾收集 ,这可能对性能产生巨大影响。

对嵌套的对象分组

如果分组的对象应包含嵌套的序列,就可以改变select子句创建的匿名类型。

var countries = from r in Formulal.GetChampions()
group r by r.Country into g
let count = g.Count()
orderby count descending, g.Key
where count >= 2
select new
{
Country = g.Key,
Count = count,
//使用内部子句嵌套
Racers = from r1 in g
orderby r1.LastName
select r1.FirstName + " " + r1.LastName
};
foreach (var item in countries)
{
Console.WriteLine($"{item.Country,-10} {item.Count}");
foreach (var name in item.Racers)
{
Console.Write(name + ";");
}
Console.WriteLine();
}

内联接

使用join子句可以根据特定的条件合并两个数据源,需要指定两个要联接的列表。

var racers = from r in Formulal.GetChampions()
from y in r.Years
select new
{
Year = y,
Name = r.FirstName + " " + r.LastName
};
var teams = from t in Formulal.GetContructorChampions()
from y in t.Years
select new
{
Year = y,
Name = t.Name
}; var racersAndTeams = (from r in racers
join t in teams on r.Year equals t.Year
orderby t.Year
select new
{
r.Year,
Champion = r.Name,
Constructor = t.Name
}).Take(10);

可以将上述的两个语句合成一个LINQ查询语句 :

var racersAndTeams2 =
(from r in from r1 in Formulal.GetChampions()
from yr in r1.Years
select new
{
Year = yr,
Name = r1.FirstName + " " + r1.LastName
}
join t in
from t1 in Formulal.GetContructorChampions()
from yt in t1.Years
select new
{
Year = yt,
Name = t1.Name
}
on r.Year equals t.Year
orderby t.Year
select new
{
Year = r.Year,
Champion = r.Name,
Constructor = t.Name
}).Take(10); Console.WriteLine("输出结果:");
foreach (var item in racersAndTeams2)
{
Console.WriteLine($"{item.Year}:{item.Champion,-20} {item.Constructor}");
}

左外联接

左外部联接是这样定义的:返回第一个集合的每个元素,无论该元素在第二个集合中是否有任何相关元素。 可以使用 LINQ 通过对分组联接的结果调用 DefaultIfEmpty 方法来执行左外部联接。

在左外部联接中,将返回左侧源序列中的所有元素,即使右侧序列中没有其匹配元素也是如此。 若要在 LINQ 中执行左外部联接,请结合使用 DefaultIfEmpty 方法与分组联接,指定要在某个左侧元素不具有匹配元素时生成的默认右侧元素。 可以使用 null 作为任何引用类型的默认值,也可以指定用户定义的默认类型。

var racers = from r in Formulal.GetChampions()
from y in r.Years
select new
{
Year = y,
Name = r.FirstName + " " + r.LastName
};
var teams = from t in Formulal.GetContructorChampions()
from y in t.Years
select new
{
Year = y,
Name = t.Name
}; var racersAndTeams = (from r in racers
join t in teams on r.Year equals t.Year into rt
from t in rt.DefaultIfEmpty()
orderby r.Year
select new
{
r.Year,
Champion = r.Name,
Constructor = t == null ? "no constructor" : t.Name
}).Take(10);
Console.WriteLine("输出结果:");
foreach (var item in racersAndTeams)
{
Console.WriteLine($"{item.Year}:{item.Champion,-10} {item.Constructor}");
}

组联接

含有 into 表达式的 join 子句称为分组联接。

使用组联接时,可以联接两个独立的序列,对于其中一个序列中的某个元素,另一个序列中存在对应的一个项列表。

分组联接会生成分层的结果序列,该序列将左侧源序列中的元素与右侧源序列中的一个或多个匹配元素相关联。 分组联接没有等效的关系术语;它本质上是一个对象数组序列。

如果在右侧源序列中找不到与左侧源中的元素相匹配的元素,则 join 子句会为该项生成一个空数组。 因此,分组联接基本上仍然是一种内部同等联接,区别在于分组联接将结果序列组织为多个组。

如果只选择分组联接的结果,则可访问各项,但无法识别结果所匹配的项。 因此,通常更为有用的做法是:选择分组联接的结果并将其放入一个也包含该项名的新类型中。

var q = (from r in Formulal.GetChampions()
join r2 in racers on
new
{
FirstName = r.FirstName,
LastName = r.LastName
}
equals
new
{
r2.FirstName,
r2.LastName
}
into yearResults
select new
{
r.FirstName,
r.LastName,
r.Wins,
r.Starts,
Results = yearResults
}); foreach(var r in q)
{
Console.WriteLine(r.FirstName+" "+r.LastName);
foreach(var results in r.Results)
{
Console.WriteLine(results.Year +" "+results.Positon);
}
}

使用扩展方法可以调用SelectMany()方法,该方法可以将序列的每个元素映射到IEnumerable<T>,并将生成的序列展平为一个序列。

var racers = Formulal.GetChampionships()
.SelectMany(cs => new List<RacerInfo>()
{
new RacerInfo
{
Year=cs.Year,
Positon=1,
FirstName=cs.First.FirstName(),
LastName=cs.First.LastName()
},
new RacerInfo
{
Year=cs.Year,
Positon=2,
FirstName=cs.Second.FirstName(),
LastName=cs.Second.LastName()
},
new RacerInfo
{
Year=cs.Year,
Positon=3,
FirstName=cs.Third.FirstName(),
LastName=cs.Third.LastName()
}
}); foreach(var r in racers)
{
Console.WriteLine(r.FirstName+" "+r.LastName);
}

集合操作

  • Intersect():通过使用的默认相等比较器对值进行比较,生成两个序列的交集。
  • Distinct():返回序列中通过使用指定的非重复元素。
  • Union():通过使用默认的相等比较器生成的两个序列的并集。
  • Except():通过使用默认的相等比较器对值进行比较,生成两个序列的差集。
Func<string, IEnumerable<Racer>> racersByCar =
car => from r in Formulal.GetChampions()
from c in r.Cars
where c == car
orderby r.LastName
select r; Console.WriteLine("调用Intersect()方法,显示结果:");
foreach(var racer in racersByCar("Ferrari").Intersect(racersByCar("Lotus")))
{
Console.WriteLine(racer);
}

合并(Zip)

Zip()将指定的函数应用于两个序列的相应元素,生成一系列结果。

var racernames = from r in Formulal.GetChampions()
where r.Country == "Italy"
orderby r.Wins descending
select new
{
Name = r.FirstName + " " + r.LastName
};
var racerNamesAndStarts = from r in Formulal.GetChampions()
where r.Country == "Italy"
orderby r.Wins descending
select new
{
LastName = r.LastName,
Starts = r.Starts
};
//第一个集合中的第一项会与第二个集合中的第一项合并
//第一个集合中的第二项会与第二个集合中的第二项合并,依次类推
//如果两个序列的项数不同,Zip()方法就在到达较小集合的末尾时停止
var racers = racernames.Zip(racerNamesAndStarts
, (first, second) => first.Name + ", starts: " + second.Starts);

分区(Take和TakeWhile,Skip和SkipWhile)

  • Take():从序列的开头返回指定的数量的连续元素。
  • TakeWhile():只要指定的条件为true,就返回序列中的元素,然后跳过其余元素。
  • Skip():跳过指定的数量的序列中的元素,然后返回剩余元素。
  • SkipWhile():只要指定的条件为真,就会跳过序列中的元素,然后返回剩余的元素。
int pageSize = 5;
int numberPages = (int)Math.Ceiling(Formulal.GetChampions().Count() / (double)pageSize); for (int page = 0; page < numberPages; page++)
{
Console.WriteLine("Page "+page); var racers = (from r in Formulal.GetChampions()
orderby r.LastName, r.FirstName
select r.FirstName + " " + r.LastName)
.Skip(page * pageSize).Take(pageSize); foreach(var name in racers)
{
Console.WriteLine(name);
}
Console.WriteLine( );
}

聚合操作符

聚合操作符(如CountSumMinMaxAverageAggregate操作符)不返回一个序列,而返回一个值。

  • Count():返回序列中的元素数。
  • Sum():计算一系列数值的总和。
  • Min():返回值序列中的最小值。
  • Max():返回值序列中的最大值。
  • Average():计算一系列数值的平均值。
  • Aggregate(): 对一个序列应用累加器函数。
Console.WriteLine("Count():");
var query = from r in Formulal.GetChampions()
let numberYears = r.Years.Count()
where numberYears >= 3
orderby numberYears descending, r.LastName
select new
{
Name = r.FirstName + " " + r.LastName,
TimesChampion = numberYears
}; foreach(var r in query)
{
Console.WriteLine(r.Name+" "+r.TimesChampion);
} Console.WriteLine();
Console.WriteLine("Sum():"); var countries = (from c in from r in Formulal.GetChampions()
group r by r.Country into c
select new
{
Country = c.Key,
Wins = (from r1 in c select r1.Wins).Sum()
}
orderby c.Wins descending, c.Country
select c).Take(5);
foreach(var country in countries)
{
Console.WriteLine(country.Country+" "+country.Wins);
}

转换操作符(ToList、ToLookup、Cast)

在之前的内容曾提到,查询可以推迟到访问数据项时再执行。在迭代中使用查询时,查询会执行。而使用转换操作符会立即执行查询,把查询结果放在数组、列表或字典中。

  • ToList():从IEnumerable<T>创建List<T>
  • ToLookup():从IEnumerable <T>创建一个通用的Lookup <TKek,TElement >。注意:Dictionary<TKey,TValue>类只支持一个键对应一个值。类Lookup<TKey,TElement>中,一个键可以对应多个值。
  • Cast():将IEnumerable的元素转换为指定的类型。
List<Racer> racers = (from r in Formulal.GetChampions()
where r.Starts > 150
orderby r.Starts descending
select r).ToList();
//ToLookup
var racers2 = (from r in Formulal.GetChampions()
from c in r.Cars
select new
{
Car = c,
Racer = r
})
.ToLookup(cr => cr.Car, cr => cr.Racer);
if (racers2.Contains("Williams"))
{
foreach (var winll in racers2["Williams"])
{
Console.WriteLine(winll);
}
}

如果需要在非类型化的集合上(如ArrayList)使用LINQ查询,就可以使用Cast()方法。

var list = new System.Collections.ArrayList(Formulal.GetChampions()
as System.Collections.ICollection);
//基于Object类型的ArrayList集合用Racer对象填充
var query = from r in list.Cast<Racer>()
where r.Country == "China"
orderby r.Wins descending
select r;

生成操作符(Range、Empty、Repeat)

生成操作符Range()Empty()Repeat()不是扩展方法,而是返回序列的正常静态方法。在LINQ to Objects中,这些方法可以用于Enumerable类。

  • Range():生成指定范围内的整数序列。 注意:该方法不返回填充了所定义值的集合,这个方法与其他方法一样,也推迟执行查询。
  • Empty():返回具有指定类型参数的空IEnumerable <T>。它可以用于需要一个集合的参数,其中可以给参数传递空集合。
  • Repeat():生成包含一个重复值的序列。
var values = Enumerable.Range(1, 20);
foreach (var item in values)
{
Console.WriteLine($"{item}");
}

并行LINQ(Parallel LINQ,PLINQ)

System.Linq命名空间中包含的类ParallelEnumerable可以分解查询的工作,使其分布在多个线程上。

ParallelEnumerable类的大多数扩展方法是基于ParallelQuery<TSource>类的扩展。在ParallelEnumerable类中,存在一个重要的方法AsParallel(),该方法扩展了IEnumerable<TSource>接口,所以正常的集合类都可以调用该方法以并行方式查询。

public static ParallelQuery<TSource> AsParallel<TSource>(this IEnumerable<TSource> source);
public static ParallelQuery<TSource> AsParallel<TSource>(this Partitioner<TSource> source);
public static ParallelQuery AsParallel(this IEnumerable source);

一般使用并行LINQ,需要一个大型集合,对于可以放在CPU的缓存中的小集合,并行LINQ看不出效果。

public static IEnumerable<int> SampleData()
{
const int arraySize = 50000000;
var r = new Random();
//连续50000000次随机取出小于140的数字
return Enumerable.Range(0, arraySize).Select(x => r.Next(140)).ToList();
}
var data = SampleData();
//查询表达式写法
var res = (from x in data.AsParallel() where Math.Log(x) < 4 select x).Average();
//扩展方法写法
var res2 = data.AsParallel().Where(x => Math.Log(x) < 4).Select(x => x).Average();

上述AsParallel()方法返回ParallelQuery<TSource>,之后的WhereSelectAverage都是来自于ParallelEnumerable类。对于ParallelEnumerable类,查询是分区的,以便多个线程可以同时处理该查询。集合可以分为多个部分,其中每个部分由不同的线程处理,以筛选其余项。完成分区的工作后,就需要合并,获得所有部分的总和。

分区器

AsParallel()方法不仅扩展了IEnumerable<TSource>接口,还扩展了Partitioner<TSource>类。

Partitioner类用于为数组,列表和枚举提供通用的分区策略。 该类只提供了Create()方法,Create()方法有很多的变体,返回的是OrderablePartitioner<TSource>,而OrderablePartitioner<TSource>继承自Partitioner<TSource>,所以通过调用Partitioner.Create()可以影响要创建的分区。

Partitioner.Create()重载方法如下:

public static OrderablePartitioner<TSource> Create<TSource>(IList<TSource> list, bool loadBalance);
public static OrderablePartitioner<TSource> Create<TSource>(TSource[] array, bool loadBalance);
public static OrderablePartitioner<TSource> Create<TSource>(IEnumerable<TSource> source);
public static OrderablePartitioner<TSource> Create<TSource>(IEnumerable<TSource> source, EnumerablePartitionerOptions partitionerOptions);
public static OrderablePartitioner<Tuple<long, long>> Create(long fromInclusive, long toExclusive);
public static OrderablePartitioner<Tuple<long, long>> Create(long fromInclusive, long toExclusive, long rangeSize);
public static OrderablePartitioner<Tuple<int, int>> Create(int fromInclusive, int toExclusive);
public static OrderablePartitioner<Tuple<int, int>> Create(int fromInclusive, int toExclusive, int rangeSize);

Create()方法接受实现了ILIst<T>类的数组或对象。根据这一点,以及Boolean类型的参数loadBalance和该方法的一些重载版本,会返回一个不同的Partitioner类型(Partitioner<TSource>)。对于数组,使用派生自抽象基类OrderablePartitioner<TSource>DynamicPartitionerForArray<TSource>类和StaticPartitionerForArray<TSource>类。

var result= (from x in Partitioner.Create((List<int>)data, true)
.AsParallel() where Math.Log(x) < 4 select x)
.Average();

也可以调用WithExecutionMode()WithDegreeOfParallelism()方法,来影响并行机制。对于WithExecutionMode()方法可以传递ParallelExecutionMode的一个Default值或者ForceParallelism值。默认情况下,并行LINQ避免使用系统开销很高的并行机制。对于WithDegreeOfParallelism()方法,可以传递一个整数值,以指定应并行运行的最大任务数。查询不应使用全部CPU,这个方法会很有用。

取消

.NET提供了一中标准方式,来取消长时间运行的任务。这也适用于并行LINQ。

要取消长时间运行的查询,可以给查询添加WithCancellation()方法,并传递一个CancellationToken令牌作为参数。CancellationToken令牌从CancellationTokenSource类中创建。该查询在单独的线程中运行,在该线程中,捕获一个OperationCanceledException类型的异常。如果取消了查询,就触发这个异常。在主线程中,调用CancellationTokenSource类的Cancel()方法可以取消任务。

var cts = new CancellationTokenSource();
var data = SampleData();
Task.Run(()=> {
try
{
var res = (from x in data.AsParallel().WithCancellation(cts.Token)
where Math.Log(x) < 4
select x).Average();
Console.WriteLine(res);
}
catch (OperationCanceledException ex)
{
Console.WriteLine(ex.Message);
} }); Console.WriteLine("取消吗?");
string input = Console.ReadLine();
if (input.ToLower().Equals("y"))
{
cts.Cancel();
}

表达式树(Expression<T>

如果一个方法的参数是Expression<T>类型,当把lambda表达式赋予这个参数时,C#编译器就会从lambda表达式中创建一个表达式树,并存储在程序集中,这样,就可以在运行期间分析表达式树,并进行优化,以便于查询数据源。表达式树从派生自抽象基类Expression的类中构建。Expression类与Expression<T>不同。继承自Expression类的表达式有BinaryExpressionConstantExpressionInvocationExpressionLambdaExpressionNewExpressionNewArrayExpressionTernaryExpression以及UnaryExpression等。编译器会从Lambda表达式中创建表达式树。

C#基础提升系列——C# LINQ的更多相关文章

  1. C#基础提升系列——C#异步编程

    C#异步编程 关于异步的概述,这里引用MSDN的一段文字: 异步编程是一项关键技术,使得能够简单处理多个核心上的阻塞 I/O 和并发操作. 如果需要 I/O 绑定(例如从网络请求数据或访问数据库),则 ...

  2. C#基础提升系列——C#文件和流

    C#文件和流 本文主要是对C#中的流进行详细讲解,关于C#中的文件操作,考虑到后期.net core跨平台,相关操作可能会发生很大变化,所以此处不对文件系统(包括目录.文件)过多的讲解,只会描述出在. ...

  3. C#基础提升系列——C#委托

    C# 委托 委托是类型安全的类,它定义了返回类型和参数的类型,委托类可以包含一个或多个方法的引用.可以使用lambda表达式实现参数是委托类型的方法. 委托 当需要把一个方法作为参数传递给另一个方法时 ...

  4. C#基础提升系列——C#集合

    C#集合 有两种主要的集合类型:泛型集合和非泛型集合. 泛型集合被添加在 .NET Framework 2.0 中,并提供编译时类型安全的集合. 因此,泛型集合通常能提供更好的性能. 构造泛型集合时, ...

  5. C# 基础知识系列-7 Linq详解

    前言 在上一篇中简单介绍了Linq的入门级用法,这一篇尝试讲解一些更加深入的使用方法,与前一篇的结构不一样的地方是,这一篇我会先介绍Linq里的支持方法,然后以实际需求为引导,分别以方法链的形式和类S ...

  6. C#基础提升系列——C#任务和并行编程

    C#任务和并行编程 我们在处理有些需要等待的操作时,例如,文件读取.数据库或网络访问等,这些都需要一定的时间,我们可以使用多线程,不需要让用户一直等待这些任务的完成,就可以同时执行其他的一些操作.即使 ...

  7. C#基础提升系列——C#任务同步

    C#任务同步 如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态.如果不注意同步,就会出现争用条件和死锁. 不同步导致的线程问题 如果两个或多个线程访问相同的对象,并且对共享 ...

  8. C#基础提升系列——C# 泛型

    C# 泛型(Generics) 泛型概述 泛型是C#编程语言的一部分,它与程序集中的IL(Intermediate Language,中间语言)代码紧密的集成.通过泛型,我们不必给不同的类型编写功能相 ...

  9. 【JAVA零基础入门系列】Day12 Java类的简单应用

    俗话说的好,实践出真知,所以除了理论知识掌握扎实以外,更重要的是要多加操练,这样才能掌握核心科技. 今天我们就用刚学会的类来实践一下,目标便是完成上一篇中的剁手任务. 我们的商品类已经准备好了,代码重 ...

随机推荐

  1. 攻防世界 | level0

    先反编译 : int __cdecl main(int argc, const char **argv, const char **envp) { write(1, "Hello, Worl ...

  2. 给url添加时间戳,解决浏览器缓存

    //解决浏览器缓存function timestamp(url){ // var getTimestamp=Math.random(); var getTimestamp=new Date().get ...

  3. SEC3 - MySQL常见命令

    1.查看当前所有的数据库 show databases; 2. 打开指定的库名 use 库名称: 3.查看当前库中所有的表 show tables; 4. 查看其他库的所有表 show tables ...

  4. Web控件中Eval()的使用

    1.使用Eval()绑定数据时使用三元运算符 <%#Eval("hg_A").ToString()=="1"?"通过":Eval(&q ...

  5. selenium python 报错“ unable to find binary in default location”

    selenium python 报错如下: raise exception_class(message, screen, stacktrace)selenium.common.exceptions.W ...

  6. mysql 主从复制 (2)

    今天说一下MySQL的主从复制如何做到! 准备工作: 1.两个虚拟机:我这里用的是CentOS5.5,IP地址分别是192.168.1.101 和192.168.1.105: 101做主服务器,105 ...

  7. from、includes、indexOf

    from.includes.indexOf:https://blog.csdn.net/j59580/article/details/53897630?utm_source=blogxgwz1 语法 ...

  8. mybatis问题整理

    // List<String> findBuildByProject(String prjName); //单参数时使用<if></if>标签判断采用"_ ...

  9. Linux服务正常启动,Linux服务器能访问,但是外部机器不能访问

    公司用到了jenkins,就在自己虚拟机里面部署了一个jenkins.部署成功之后,在Linux虚拟机里面能正常访问,但是外部真实机却不能访问.当时的第一反应就是觉得应该是权限问题,猜测会不会是jen ...

  10. [暑假集训Day2T2]走廊泼水节

    给定一棵n个点的图上的最小生成树,让你把它补成完全图,使得新图的MST还是给定的MST且边权和最小,输出需要增加的边权和. 设size[i]表示以i号为祖先的并查集的大小. 首先按边权排序,之后在做M ...