摘自https://msdn.microsoft.com/zh-cn/library/bb310804.aspx,方便以后翻阅。

from子句

查询表达式必须以 from 子句开头。另外,查询表达式还可以包含子查询,子查询也是以 from 子句开头。 from 子句指定以下内容:

  • 将对其运行查询或子查询的数据源。
  • 一个本地范围变量,表示源序列中的每个元素。

范围变量和数据源都是强类型。 from 子句中引用的数据源的类型必须为 IEnumerable、IEnumerable<T> 或一种派生类型(如 IQueryable<T>)
在下面的示例中,numbers 是数据源,而 num 是范围变量。请注意,这两个变量都是强类型,即使使用了 var 关键字也是如此。

class LowNums
{
static void Main()
{
// A simple data source.
int[] numbers = { , , , , , , , , , }; // Create the query.
// lowNums is an IEnumerable<int>
var lowNums = from num in numbers
where num <
select num; // Execute the query.
foreach (int i in lowNums)
{
Console.Write(i + " ");
}
}
}
// Output: 4 1 3 2 0

复合 from 子句

在某些情况下,源序列中的每个元素本身可能是序列,也可能包含序列。例如,数据源可能是一个 IEnumerable<Student>,其中,序列中的每个 Student 对象都包含一个测验得分列表。若要访问每个 Student 元素中的内部
列表,可以使用复合 from 子句。该技术类似于使用嵌套的 foreach 语句。可以向任一 from 子句中添加 where 或 orderby 子句来筛选结果。下面的示例演示了一个 Student 对象序列,其中每个对象都包含一个表示测验

得分的内部整数 List。为了访问该内部列表,此示例使用了复合 from 子句。如有必要,可在两个 from 子句之间再插入子句。

class CompoundFrom
{
// The element type of the data source.
public class Student
{
public string LastName { get; set; }
public List<int> Scores {get; set;}
} static void Main()
{ // Use a collection initializer to create the data source. Note that
// each element in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {LastName="Omelchenko", Scores= new List<int> {, , , }},
new Student {LastName="O'Donnell", Scores= new List<int> {, , , }},
new Student {LastName="Mortensen", Scores= new List<int> {, , , }},
new Student {LastName="Garcia", Scores= new List<int> {, , , }},
new Student {LastName="Beebe", Scores= new List<int> {, , , }}
}; // Use a compound from to access the inner sequence within each element.
// Note the similarity to a nested foreach statement.
var scoreQuery = from student in students
from score in student.Scores
where score >
select new { Last = student.LastName, score }; // Execute the queries.
Console.WriteLine("scoreQuery:");
// Rest the mouse pointer on scoreQuery in the following line to
// see its type. The type is IEnumerable<'a>, where 'a is an
// anonymous type defined as new {string Last, int score}. That is,
// each instance of this anonymous type has two members, a string
// (Last) and an int (score).
foreach (var student in scoreQuery)
{
Console.WriteLine("{0} Score: {1}", student.Last, student.score);
} // Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/*
scoreQuery:
Omelchenko Score: 97
O'Donnell Score: 91
Mortensen Score: 94
Garcia Score: 97
Beebe Score: 91
*/

使用多个 from 子句执行联接
复合 from 子句用于访问单个数据源中的内部集合。不过,查询还可以包含多个可从独立数据源生成补充查询的 from 子句。使用此技术可以执行某些类型的、无法通过使用 join 子句执行的联接操作。
下面的示例演示如何使用两个 from 子句构成两个数据源的完全交叉联接

class CompoundFrom2
{
static void Main()
{
char[] upperCase = { 'A', 'B', 'C' };
char[] lowerCase = { 'x', 'y', 'z' }; // The type of joinQuery1 is IEnumerable<'a>, where 'a
// indicates an anonymous type. This anonymous type has two
// members, upper and lower, both of type char.
var joinQuery1 =
from upper in upperCase
from lower in lowerCase
select new { upper, lower }; // The type of joinQuery2 is IEnumerable<'a>, where 'a
// indicates an anonymous type. This anonymous type has two
// members, upper and lower, both of type char.
var joinQuery2 =
from lower in lowerCase
where lower != 'x'
from upper in upperCase
select new { lower, upper }; // Execute the queries.
Console.WriteLine("Cross join:");
// Rest the mouse pointer on joinQuery1 to verify its type.
foreach (var pair in joinQuery1)
{
Console.WriteLine("{0} is matched to {1}", pair.upper, pair.lower);
} Console.WriteLine("Filtered non-equijoin:");
// Rest the mouse pointer over joinQuery2 to verify its type.
foreach (var pair in joinQuery2)
{
Console.WriteLine("{0} is matched to {1}", pair.lower, pair.upper);
} // Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Cross join:
A is matched to x
A is matched to y
A is matched to z
B is matched to x
B is matched to y
B is matched to z
C is matched to x
C is matched to y
C is matched to z
Filtered non-equijoin:
y is matched to A
y is matched to B
y is matched to C
z is matched to A
z is matched to B
z is matched to C
*/

where子句

where 子句用在查询表达式中,用于指定将在查询表达式中返回数据源中的哪些元素。它将一个布尔条件(“谓词”)应用于每个源元素(由范围变量引用),并返回满足指定条件的元素。一个查询表达式可以包含多个 where
子句,一个子句可以包含多个谓词子表达式。
在下面的示例中,where 子句筛选出除小于五的数字外的所有数字。如果移除 where 子句,则会返回数据源中的所有数字。表达式 num < 5 是应用于每个元素的谓词。

class WhereSample
{
static void Main()
{
// Simple data source. Arrays support IEnumerable<T>.
int[] numbers = { , , , , , , , , , }; // Simple query with one predicate in where clause.
var queryLowNums =
from num in numbers
where num <
select num; // Execute the query.
foreach (var s in queryLowNums)
{
Console.Write(s.ToString() + " ");
}
}
}
//Output: 4 1 3 2 0

在单一 where 子句内,可以使用 && 和 || 运算符根据需要指定任意多个谓词。在下面的示例中,查询将指定两个谓词,以便只选择小于五的偶数。

class WhereSample2
{
static void Main()
{
// Data source.
int[] numbers = { , , , , , , , , , }; // Create the query with two predicates in where clause.
var queryLowNums2 =
from num in numbers
where num < && num % ==
select num; // Execute the query
foreach (var s in queryLowNums2)
{
Console.Write(s.ToString() + " ");
}
Console.WriteLine(); // Create the query with two where clause.
var queryLowNums3 =
from num in numbers
where num <
where num % ==
select num; // Execute the query
foreach (var s in queryLowNums3)
{
Console.Write(s.ToString() + " ");
} }
}
// Output:
// 4 2 0
// 4 2 0

where 子句可以包含一个或多个返回布尔值的方法。在下面的示例中,where 子句使用一个方法来确定范围变量的当前值是偶数还是奇数。

class WhereSample3
{
static void Main()
{
// Data source
int[] numbers = { , , , , , , , , , }; // Create the query with a method call in the where clause.
// Note: This won't work in LINQ to SQL unless you have a
// stored procedure that is mapped to a method by this name.
var queryEvenNums =
from num in numbers
where IsEven(num)
select num; // Execute the query.
foreach (var s in queryEvenNums)
{
Console.Write(s.ToString() + " ");
}
} // Method may be instance method or static method.
static bool IsEven(int i)
{
return i % == ;
}
}
//Output: 4 8 6 2 0

备注
where 子句是一种筛选机制。除了不能是第一个或最后一个子句外,它几乎可以放在查询表达式中的任何位置。 where 子句可以出现在 group 子句的前面或后面,具体情况取决于是必须在对源元素进行分组之前还是之后来筛

选源元素。
如果指定的谓词对于数据源中的元素无效,则会发生编译时错误。这是 LINQ 提供的强类型检查的一个优点。
编译时,where 关键字会被转换为对 Where 标准查询运算符方法的调用。


select子句

在查询表达式中,select 子句可以指定将在执行查询时产生的值的类型。该子句的结果将基于前面所有子句的计算结果以及 select 子句本身中的所有表达式。查询表达式必须以 select 子句或 group 子句结束。
下面的示例演示了查询表达式中的简单 select 子句。

class SelectSample1
{
static void Main()
{
//Create the data source
List<int> Scores = new List<int>() { , , , }; // Create the query.
IEnumerable<int> queryHighScores =
from score in Scores
where score >
select score; // Execute the query.
foreach (int i in queryHighScores)
{
Console.Write(i + " ");
}
}
}
//Output: 97 92 81

select 子句产生的序列的类型决定了查询变量 queryHighScores 的类型。在最简单的情况下,select 子句仅指定范围变量。这会使返回的序列包含与数据源具有相同类型的元素。不过,select 子句还提供了一种功能强大
的机制,可用于将源数据转换(或投影)为新类型。
下面的示例演示了 select 子句可能采用的所有不同形式。在每个查询中,请注意 select 子句和查询变量(studentQuery1、studentQuery2 等)的类型之间的关系。

class SelectSample2
{
// Define some classes
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
public ContactInfo GetContactInfo(SelectSample2 app, int id)
{
ContactInfo cInfo =
(from ci in app.contactList
where ci.ID == id
select ci)
.FirstOrDefault(); return cInfo;
} public override string ToString()
{
return First + " " + Last + ":" + ID;
}
} public class ContactInfo
{
public int ID { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public override string ToString() { return Email + "," + Phone; }
} public class ScoreInfo
{
public double Average { get; set; }
public int ID { get; set; }
} // The primary data source
List<Student> students = new List<Student>()
{
new Student {First="Svetlana", Last="Omelchenko", ID=, Scores= new List<int>() {, , , }},
new Student {First="Claire", Last="O'Donnell", ID=, Scores= new List<int>() {, , , }},
new Student {First="Sven", Last="Mortensen", ID=, Scores= new List<int>() {, , , }},
new Student {First="Cesar", Last="Garcia", ID=, Scores= new List<int>() {, , , }},
}; // Separate data source for contact info.
List<ContactInfo> contactList = new List<ContactInfo>()
{
new ContactInfo {ID=, Email="SvetlanO@Contoso.com", Phone="206-555-0108"},
new ContactInfo {ID=, Email="ClaireO@Contoso.com", Phone="206-555-0298"},
new ContactInfo {ID=, Email="SvenMort@Contoso.com", Phone="206-555-1130"},
new ContactInfo {ID=, Email="CesarGar@Contoso.com", Phone="206-555-0521"}
}; static void Main(string[] args)
{
SelectSample2 app = new SelectSample2(); // Produce a filtered sequence of unmodified Students.
IEnumerable<Student> studentQuery1 =
from student in app.students
where student.ID >
select student; Console.WriteLine("Query1: select range_variable");
foreach (Student s in studentQuery1)
{
Console.WriteLine(s.ToString());
} // Produce a filtered sequence of elements that contain
// only one property of each Student.
IEnumerable<String> studentQuery2 =
from student in app.students
where student.ID >
select student.Last; Console.WriteLine("\r\n studentQuery2: select range_variable.Property");
foreach (string s in studentQuery2)
{
Console.WriteLine(s);
} // Produce a filtered sequence of objects created by
// a method call on each Student.
IEnumerable<ContactInfo> studentQuery3 =
from student in app.students
where student.ID >
select student.GetContactInfo(app, student.ID); Console.WriteLine("\r\n studentQuery3: select range_variable.Method");
foreach (ContactInfo ci in studentQuery3)
{
Console.WriteLine(ci.ToString());
} // Produce a filtered sequence of ints from
// the internal array inside each Student.
IEnumerable<int> studentQuery4 =
from student in app.students
where student.ID >
select student.Scores[]; Console.WriteLine("\r\n studentQuery4: select range_variable[index]");
foreach (int i in studentQuery4)
{
Console.WriteLine("First score = {0}", i);
} // Produce a filtered sequence of doubles
// that are the result of an expression.
IEnumerable<double> studentQuery5 =
from student in app.students
where student.ID >
select student.Scores[] * 1.1; Console.WriteLine("\r\n studentQuery5: select expression");
foreach (double d in studentQuery5)
{
Console.WriteLine("Adjusted first score = {0}", d);
} // Produce a filtered sequence of doubles that are
// the result of a method call.
IEnumerable<double> studentQuery6 =
from student in app.students
where student.ID >
select student.Scores.Average(); Console.WriteLine("\r\n studentQuery6: select expression2");
foreach (double d in studentQuery6)
{
Console.WriteLine("Average = {0}", d);
} // Produce a filtered sequence of anonymous types
// that contain only two properties from each Student.
var studentQuery7 =
from student in app.students
where student.ID >
select new { student.First, student.Last }; Console.WriteLine("\r\n studentQuery7: select new anonymous type");
foreach (var item in studentQuery7)
{
Console.WriteLine("{0}, {1}", item.Last, item.First);
} // Produce a filtered sequence of named objects that contain
// a method return value and a property from each Student.
// Use named types if you need to pass the query variable
// across a method boundary.
IEnumerable<ScoreInfo> studentQuery8 =
from student in app.students
where student.ID >
select new ScoreInfo
{
Average = student.Scores.Average(),
ID = student.ID
}; Console.WriteLine("\r\n studentQuery8: select new named type");
foreach (ScoreInfo si in studentQuery8)
{
Console.WriteLine("ID = {0}, Average = {1}", si.ID, si.Average);
} // Produce a filtered sequence of students who appear on a contact list
// and whose average is greater than 85.
IEnumerable<ContactInfo> studentQuery9 =
from student in app.students
where student.Scores.Average() >
join ci in app.contactList on student.ID equals ci.ID
select ci; Console.WriteLine("\r\n studentQuery9: select result of join clause");
foreach (ContactInfo ci in studentQuery9)
{
Console.WriteLine("ID = {0}, Email = {1}", ci.ID, ci.Email);
} // Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output
Query1: select range_variable
Claire O'Donnell:112
Sven Mortensen:113
Cesar Garcia:114 studentQuery2: select range_variable.Property
O'Donnell
Mortensen
Garcia studentQuery3: select range_variable.Method
ClaireO@Contoso.com,206-555-0298
SvenMort@Contoso.com,206-555-1130
CesarGar@Contoso.com,206-555-0521 studentQuery4: select range_variable[index]
First score = 75
First score = 88
First score = 97 studentQuery5: select expression
Adjusted first score = 82.5
Adjusted first score = 96.8
Adjusted first score = 106.7 studentQuery6: select expression2
Average = 72.25
Average = 84.5
Average = 88.25 studentQuery7: select new anonymous type
O'Donnell, Claire
Mortensen, Sven
Garcia, Cesar studentQuery8: select new named type
ID = 112, Average = 72.25
ID = 113, Average = 84.5
ID = 114, Average = 88.25 studentQuery9: select result of join clause
ID = 114, Email = CesarGar@Contoso.com
*/

如上一个示例中的 studentQuery8 所示,您有时可能希望所返回序列中的元素仅包含源元素的属性子集。通过使返回的序列尽可能地小一些,可以降低内存需求,并提高查询的执行速度。通过在 select 子句中创建一个匿名
类型,并且借助于对象初始值设定项用源元素中的适当属性对该匿名类型进行初始化,可以达到此目的。

备注
编译时,select 子句会被转换为对 Select<TSource, TResult> 标准查询运算符的方法调用。


group子句

group 子句返回一个 IGrouping<TKey, TElement> 对象序列,这些对象包含零个或更多个与该组的键值匹配的项。例如,可以按照每个字符串中的第一个字母对字符串序列进行分组。在这种情况下,第一个字母是键且具有
char 类型,并且存储在每个 IGrouping<TKey, TElement> 对象的 Key 属性中。编译器可推断该键的类型。
可以用 group 子句结束查询表达式,如下面的示例所示:

// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery1 =
from student in students
group student by student.Last[];

如果想要对每个组执行附加查询操作,则可以使用 into 上下文关键字指定一个临时标识符。使用 into 时,必须继续编写该查询,并最终用一个 select 语句或另一个 group 子句结束该查询,如下面的代码摘录所示:

// Group students by the first letter of their last name
// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery2 =
from student in students
group student by student.Last[] into g
orderby g.Key
select g;

本主题中的“示例”部分中提供了使用含有和不含 into 的 group 的更完整示例。

枚举组查询的结果
由于 group 查询产生的 IGrouping<TKey, TElement> 对象实质上是列表的列表,因此必须使用嵌套的 foreach 循环来访问每一组中的各个项。外部循环用于循环访问组键,内部循环用于循环访问组本身中的每个项。组可能

具有键,但没有元素。以下是执行上述代码示例中的查询的 foreach 循环:

// Iterate group items with a nested foreach. This IGrouping encapsulates
// a sequence of Student objects, and a Key of type char.
// For convenience, var can also be used in the foreach statement.
foreach (IGrouping<char, Student> studentGroup in studentQuery2)
{
Console.WriteLine(studentGroup.Key);
// Explicit type for student could also be used here.
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}", student.Last, student.First);
}
}

键类型
组键可以是任何类型,如字符串、内置数值类型、用户定义的命名类型或匿名类型。

  • 按字符串进行分组

上述代码示例使用的是 char。可以很容易地改为指定字符串键,如完整的姓氏:

// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;
  • 按布尔进行分组

下面的示例演示使用布尔值作为键将结果划分成两个组。请注意,该值是由 group 子句中的子表达式产生的。

class GroupSample1
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
} public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=, Scores= new List<int> {, , , }},
new Student {First="Claire", Last="O'Donnell", ID=, Scores= new List<int> {, , , }},
new Student {First="Sven", Last="Mortensen", ID=, Scores= new List<int> {, , , }},
new Student {First="Cesar", Last="Garcia", ID=, Scores= new List<int> {, , , }},
new Student {First="Debra", Last="Garcia", ID=, Scores= new List<int> {, , , }}
}; return students; } static void Main()
{
// Obtain the data source.
List<Student> students = GetStudents(); // Group by true or false.
// Query variable is an IEnumerable<IGrouping<bool, Student>>
var booleanGroupQuery =
from student in students
group student by student.Scores.Average() >= ; //pass or fail! // Execute the query and access items in each group
foreach (var studentGroup in booleanGroupQuery)
{
Console.WriteLine(studentGroup.Key == true ? "High averages" : "Low averages");
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
}
} // Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Low averages
Omelchenko, Svetlana:77.5
O'Donnell, Claire:72.25
Garcia, Cesar:75.5
High averages
Mortensen, Sven:93.5
Garcia, Debra:88.25
*/
  • 按数值范围进行分组

下一个示例使用表达式创建表示百分比范围的数值组键。请注意,该示例使用 let 作为方法调用结果的方便存储位置,从而无需在 group 子句中调用该方法两次。另请注意,在 group 子句中,为了避免发生“被零除”异常
,代码进行了相应检查以确保学生的平均成绩不为零。

class GroupSample2
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
} public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=, Scores= new List<int> {, , , }},
new Student {First="Claire", Last="O'Donnell", ID=, Scores= new List<int> {, , , }},
new Student {First="Sven", Last="Mortensen", ID=, Scores= new List<int> {, , , }},
new Student {First="Cesar", Last="Garcia", ID=, Scores= new List<int> {, , , }},
new Student {First="Debra", Last="Garcia", ID=, Scores= new List<int> {, , , }}
}; return students; } // This method groups students into percentile ranges based on their
// grade average. The Average method returns a double, so to produce a whole
// number it is necessary to cast to int before dividing by 10.
static void Main()
{
// Obtain the data source.
List<Student> students = GetStudents(); // Write the query.
var studentQuery =
from student in students
let avg = (int)student.Scores.Average()
group student by (avg == ? : avg / ) into g
orderby g.Key
select g; // Execute the query.
foreach (var studentGroup in studentQuery)
{
int temp = studentGroup.Key * ;
Console.WriteLine("Students with an average between {0} and {1}", temp, temp + );
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
}
} // Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Students with an average between 70 and 80
Omelchenko, Svetlana:77.5
O'Donnell, Claire:72.25
Garcia, Cesar:75.5
Students with an average between 80 and 90
Garcia, Debra:88.25
Students with an average between 90 and 100
Mortensen, Sven:93.5
*/
  • 按复合键进行分组

当您想要按照多个键对元素进行分组时,可使用复合键。通过使用匿名类型或命名类型来存储键元素,可以创建复合键。在下面的示例中,假定已经使用名为 surname 和 city 的两个成员声明了类 Person。 group 子句使得
为每组具有相同姓氏和相同城市的人员创建一个单独的组。

group person by new {name = person.surname, city = person.city};

如果必须将查询变量传递给其他方法,请使用命名类型。使用自动实现的属性作为键来创建一个特殊类,然后重写 Equals 和 GetHashCode 方法。还可以使用结构;在此情况下,并不绝对需要重写这些方法。

下面的示例演示在没有向组应用附加查询逻辑时将源数据排序放入不同组中的标准模式。这称为不带延续的分组。字符串数组中的元素按照它们的第一个字母进行分组。查询结果是一个 IGrouping<TKey, TElement> 类型,其
中包含一个 char 类型的公共 Key 属性以及一个包含分组中每个项的 IEnumerable<T> 集合。
group 子句的结果是序列的序列。因此,若要访问所返回的每个组中的单个元素,请在循环访问组键的循环内使用嵌套的 foreach 循环,如下面的示例所示。

class GroupExample1
{
static void Main()
{
// Create a data source.
string[] words = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" }; // Create the query.
var wordGroups =
from w in words
group w by w[]; // Execute the query.
foreach (var wordGroup in wordGroups)
{
Console.WriteLine("Words that start with the letter '{0}':", wordGroup.Key);
foreach (var word in wordGroup)
{
Console.WriteLine(word);
}
} // Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Words that start with the letter 'b':
blueberry
banana
Words that start with the letter 'c':
chimpanzee
cheese
Words that start with the letter 'a':
abacus
apple
*/

此示例演示在创建组之后,如何使用通过 into 实现的延续对这些组执行附加逻辑。下面的示例查询每个组以仅选择那些键值为元音的元素。

class GroupClauseExample2
{
static void Main()
{
// Create the data source.
string[] words2 = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese", "elephant", "umbrella", "anteater" }; // Create the query.
var wordGroups2 =
from w in words2
group w by w[] into grps
where (grps.Key == 'a' || grps.Key == 'e' || grps.Key == 'i'
|| grps.Key == 'o' || grps.Key == 'u')
select grps; // Execute the query.
foreach (var wordGroup in wordGroups2)
{
Console.WriteLine("Groups that start with a vowel: {0}", wordGroup.Key);
foreach (var word in wordGroup)
{
Console.WriteLine(" {0}", word);
}
} // Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Groups that start with a vowel: a
abacus
apple
anteater
Groups that start with a vowel: e
elephant
Groups that start with a vowel: u
umbrella
*/

备注
编译时,group 子句被转换为对 GroupBy<TSource, TKey> 方法的调用。

可以使用 into 上下文关键字创建一个临时标识符,以便将 group、join 或 select 子句的结果存储到新的标识符中。此标识符本身可以是附加查询命令的生成器。在 group 或 select 子句中使用新标识符的用法有时称为

“延续”。
下面的示例演示使用 into 关键字来启用临时标识符 fruitGroup,该标识符具有推断类型 IGrouping。通过使用该标识符,可以对每个组调用 Count<TSource> 方法,并且仅选择那些包含两个或更多个单词的组。

class IntoSample1
{
static void Main()
{ // Create a data source.
string[] words = { "apples", "blueberries", "oranges", "bananas", "apricots"}; // Create the query.
var wordGroups1 =
from w in words
group w by w[] into fruitGroup
where fruitGroup.Count() >=
select new { FirstLetter = fruitGroup.Key, Words = fruitGroup.Count() }; // Execute the query. Note that we only iterate over the groups,
// not the items in each group
foreach (var item in wordGroups1)
{
Console.WriteLine(" {0} has {1} elements.", item.FirstLetter, item.Words);
} // Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
a has 2 elements.
b has 2 elements.
*/

仅当希望对每个组执行附加查询操作时,才需要在 group 子句中使用 into。


orderby 子句
在查询表达式中,orderby 子句可使返回的序列或子序列(组)按升序或降序排序。可以指定多个键,以便执行一个或多个次要排序操作。排序是由针对元素类型的默认比较器执行的。默认排序顺序为升序。您还可以指定自定

义比较器。但是,只能通过基于方法的语法使用它。有关更多信息,请参见 Sorting Data。
在下面的示例中,第一个查询按从 A 开始的字母顺序对单词进行排序,第二个查询按降序对相同的单词进行排序。(ascending 关键字是默认排序值,可以省略。)

class OrderbySample1
{
static void Main()
{
// Create a delicious data source.
string[] fruits = { "cherry", "apple", "blueberry" }; // Query for ascending sort.
IEnumerable<string> sortAscendingQuery =
from fruit in fruits
orderby fruit //"ascending" is default
select fruit; // Query for descending sort.
IEnumerable<string> sortDescendingQuery =
from w in fruits
orderby w descending
select w; // Execute the query.
Console.WriteLine("Ascending:");
foreach (string s in sortAscendingQuery)
{
Console.WriteLine(s);
} // Execute the query.
Console.WriteLine(Environment.NewLine + "Descending:");
foreach (string s in sortDescendingQuery)
{
Console.WriteLine(s);
} // Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Ascending:
apple
blueberry
cherry Descending:
cherry
blueberry
apple
*/

下面的示例对学生的姓氏执行主要排序,然后对他们的名字执行次要排序。

class OrderbySample2
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
} public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=},
new Student {First="Claire", Last="O'Donnell", ID=},
new Student {First="Sven", Last="Mortensen", ID=},
new Student {First="Cesar", Last="Garcia", ID=},
new Student {First="Debra", Last="Garcia", ID=}
}; return students; }
static void Main(string[] args)
{
// Create the data source.
List<Student> students = GetStudents(); // Create the query.
IEnumerable<Student> sortedStudents =
from student in students
orderby student.Last ascending, student.First ascending
select student; // Execute the query.
Console.WriteLine("sortedStudents:");
foreach (Student student in sortedStudents)
Console.WriteLine(student.Last + " " + student.First); // Now create groups and sort the groups. The query first sorts the names
// of all students so that they will be in alphabetical order after they are
// grouped. The second orderby sorts the group keys in alpha order.
var sortedGroups =
from student in students
orderby student.Last, student.First
group student by student.Last[] into newGroup
orderby newGroup.Key
select newGroup; // Execute the query.
Console.WriteLine(Environment.NewLine + "sortedGroups:");
foreach (var studentGroup in sortedGroups)
{
Console.WriteLine(studentGroup.Key);
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}", student.Last, student.First);
}
} // Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
sortedStudents:
Garcia Cesar
Garcia Debra
Mortensen Sven
O'Donnell Claire
Omelchenko Svetlana sortedGroups:
G
Garcia, Cesar
Garcia, Debra
M
Mortensen, Sven
O
O'Donnell, Claire
Omelchenko, Svetlana
*/

备注
编译时,orderby 子句被转换为对 OrderBy<TSource, TKey> 方法的调用。 orderby 子句中的多个键转换为 ThenBy<TSource, TKey> 方法调用。


join子句
使用 join 子句可以将来自不同源序列并且在对象模型中没有直接关系的元素相关联。唯一的要求是每个源中的元素需要共享某个可以进行比较以判断是否相等的值。例如,食品经销商可能具有某种产品的供应商列表以及买主

列表。例如,可以使用 join 子句创建该产品同一指定地区供应商和买主的列表。
join 子句接受两个源序列作为输入。每个序列中的元素都必须是可以与另一个序列中的相应属性进行比较的属性,或者包含一个这样的属性。 join 子句使用特殊的 equals 关键字比较指定的键是否相等。 join 子句执行的

所有联接都是同等联接。 join 子句的输出形式取决于所执行的联接的具体类型。以下是三种最常见的联接类型:

  • 内部联接
  • 分组联接
  • 左外部联接

内部联接
下面的示例演示一个简单的内部同等联接。此查询产生一个“产品名称/类别”对平面序列。同一类别字符串将出现在多个元素中。如果 categories 中的某个元素不具有匹配的 products,则该类别不会出现在结果中。

var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name }; //produces flat sequence

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

var innerGroupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new { CategoryName = category.Name, Products = prodGroup };

分组联接会产生一个分层的结果序列,该序列将左侧源序列中的元素与右侧源序列中的一个或多个匹配元素相关联。分组联接没有等效的关系术语;它本质上是一个对象数组序列。
如果在右侧源序列中找不到与左侧源中的元素相匹配的元素,则 join 子句会为该项产生一个空数组。因此,分组联接基本上仍然是一种内部同等联接,区别只在于分组联接将结果序列组织为多个组。
如果您只选择分组联接的结果,则可以访问各个项,但无法识别结果所匹配的键。因此,通常更为有用的做法是选择分组联接的结果并放入一个也具有该键名的新类型中,如上一个示例所示。
当然,还可以将分组联接的结果用作其他子查询的生成器:

var innerGroupJoinQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from prod2 in prodGroup
where prod2.UnitPrice > 2.50M
select prod2;

左外部联接
在左外部联接中,将返回左侧源序列中的所有元素,即使它们在右侧序列中没有匹配的元素也是如此。若要在 LINQ 中执行左外部联接,请将 DefaultIfEmpty 方法与分组联接结合起来,以指定要在某个左侧元素不具有匹配元

素时产生的默认右侧元素。可以使用 null 作为任何引用类型的默认值,也可以指定用户定义的默认类型。下面的示例演示了用户定义的默认类型:

var leftOuterJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty(new Product { Name = String.Empty, CategoryID = })
select new { CatName = category.Name, ProdName = item.Name };

备注
后面未跟 into 的 join 子句被转换为 Join<TOuter, TInner, TKey, TResult> 方法调用;后面跟有 into 的 join 子句被转换为 GroupJoin<TOuter, TInner, TKey, TResult> 方法调用。


equals 运算符
join 子句执行同等联接。换句话说,只能基于两个键之间的相等关系进行匹配。其他类型的比较(例如,“greater than”或“not equals”)不受支持。为了表明所有联接都是同等联接,join 子句使用 equals 关键字而

不是 == 运算符。 equals 关键字只能用在 join 子句中,并且它与 == 运算符之间存在一个重要区别。对于 equals,左键使用外部源序列,而右键使用内部源序列。外部源仅在 equals 的左侧位于范围内,而内部源序列仅

在其右侧位于范围内。
非同等联接
通过使用多个 from 子句将新序列单独引入到查询中,可以执行非同等联接、交叉联接和其他自定义联接操作。
对象集合联接与关系表
在 LINQ 查询表达式中,联接操作是在对象集合上执行的。不能使用与两个关系表完全相同的方式“联接”对象集合。在 LINQ 中,仅当两个源序列没有通过任何关系相互联系时,才需要使用显式 join 子句。使用 LINQ to

SQL 时,外键表在对象模型中表示为主表的属性。例如,在 Northwind 数据库中,Customer 表与 Orders 表之间具有外键关系。在将这两个表映射到对象模型时,Customer 类具有一个 Orders 属性,该属性包含与该

Customer 相关联的 Orders 的集合。实际上,已经为您执行了联接。
复合键
使用复合键可以测试多个值是否相等。还可以在 group 子句中使用组合键。
下面的示例比较了使用相同的匹配键对相同数据源执行内部联接、分组联接和左外部联接的结果。这些示例中添加了一些额外的代码,以便在控制台显示中阐明结果。

class JoinDemonstration
{
#region Data class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
} class Category
{
public string Name { get; set; }
public int ID { get; set; }
} // Specify the first data source.
List<Category> categories = new List<Category>()
{
new Category(){Name="Beverages", ID=},
new Category(){ Name="Condiments", ID=},
new Category(){ Name="Vegetables", ID=},
new Category() { Name="Grains", ID=},
new Category() { Name="Fruit", ID=}
}; // Specify the second data source.
List<Product> products = new List<Product>()
{
new Product{Name="Cola", CategoryID=},
new Product{Name="Tea", CategoryID=},
new Product{Name="Mustard", CategoryID=},
new Product{Name="Pickles", CategoryID=},
new Product{Name="Carrots", CategoryID=},
new Product{Name="Bok Choy", CategoryID=},
new Product{Name="Peaches", CategoryID=},
new Product{Name="Melons", CategoryID=},
};
#endregion static void Main(string[] args)
{
JoinDemonstration app = new JoinDemonstration(); app.InnerJoin();
app.GroupJoin();
app.GroupInnerJoin();
app.GroupJoin3();
app.LeftOuterJoin();
app.LeftOuterJoin2(); // Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
} void InnerJoin()
{
// Create the query that selects
// a property from each element.
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { Category = category.ID, Product = prod.Name }; Console.WriteLine("InnerJoin:");
// Execute the query. Access results
// with a simple foreach statement.
foreach (var item in innerJoinQuery)
{
Console.WriteLine("{0,-10}{1}", item.Product, item.Category);
}
Console.WriteLine("InnerJoin: {0} items in 1 group.", innerJoinQuery.Count());
Console.WriteLine(System.Environment.NewLine); } void GroupJoin()
{
// This is a demonstration query to show the output
// of a "raw" group join. A more typical group join
// is shown in the GroupInnerJoin method.
var groupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup; // Store the count of total items (for demonstration only).
int totalItems = ; Console.WriteLine("Simple GroupJoin:"); // A nested foreach statement is required to access group items.
foreach (var prodGrouping in groupJoinQuery)
{
Console.WriteLine("Group:");
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine("Unshaped GroupJoin: {0} items in {1} unnamed groups", totalItems, groupJoinQuery.Count());
Console.WriteLine(System.Environment.NewLine);
} void GroupInnerJoin()
{
var groupJoinQuery2 =
from category in categories
orderby category.ID
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new
{
Category = category.Name,
Products = from prod2 in prodGroup
orderby prod2.Name
select prod2
}; //Console.WriteLine("GroupInnerJoin:");
int totalItems = ; Console.WriteLine("GroupInnerJoin:");
foreach (var productGroup in groupJoinQuery2)
{
Console.WriteLine(productGroup.Category);
foreach (var prodItem in productGroup.Products)
{
totalItems++;
Console.WriteLine(" {0,-10} {1}", prodItem.Name, prodItem.CategoryID);
}
}
Console.WriteLine("GroupInnerJoin: {0} items in {1} named groups", totalItems, groupJoinQuery2.Count());
Console.WriteLine(System.Environment.NewLine);
} void GroupJoin3()
{ var groupJoinQuery3 =
from category in categories
join product in products on category.ID equals product.CategoryID into prodGroup
from prod in prodGroup
orderby prod.CategoryID
select new { Category = prod.CategoryID, ProductName = prod.Name }; //Console.WriteLine("GroupInnerJoin:");
int totalItems = ; Console.WriteLine("GroupJoin3:");
foreach (var item in groupJoinQuery3)
{
totalItems++;
Console.WriteLine(" {0}:{1}", item.ProductName, item.Category);
} Console.WriteLine("GroupJoin3: {0} items in 1 group", totalItems, groupJoinQuery3.Count());
Console.WriteLine(System.Environment.NewLine);
} void LeftOuterJoin()
{
// Create the query.
var leftOuterQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup.DefaultIfEmpty(new Product() { Name = "Nothing!", CategoryID = category.ID }); // Store the count of total items (for demonstration only).
int totalItems = ; Console.WriteLine("Left Outer Join:"); // A nested foreach statement is required to access group items
foreach (var prodGrouping in leftOuterQuery)
{
Console.WriteLine("Group:", prodGrouping.Count());
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine("LeftOuterJoin: {0} items in {1} groups", totalItems, leftOuterQuery.Count());
Console.WriteLine(System.Environment.NewLine);
} void LeftOuterJoin2()
{
// Create the query.
var leftOuterQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty()
select new { Name = item == null ? "Nothing!" : item.Name, CategoryID = category.ID }; Console.WriteLine("LeftOuterJoin2: {0} items in 1 group", leftOuterQuery2.Count());
// Store the count of total items
int totalItems = ; Console.WriteLine("Left Outer Join 2:"); // Groups have been flattened.
foreach (var item in leftOuterQuery2)
{
totalItems++;
Console.WriteLine("{0,-10}{1}", item.Name, item.CategoryID);
}
Console.WriteLine("LeftOuterJoin2: {0} items in 1 group", totalItems);
}
}
/*Output: InnerJoin:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Peaches 5
Melons 5
InnerJoin: 8 items in 1 group. Unshaped GroupJoin:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Group:
Peaches 5
Melons 5
Unshaped GroupJoin: 8 items in 5 unnamed groups GroupInnerJoin:
Beverages
Cola 1
Tea 1
Condiments
Mustard 2
Pickles 2
Vegetables
Bok Choy 3
Carrots 3
Grains
Fruit
Melons 5
Peaches 5
GroupInnerJoin: 8 items in 5 named groups GroupJoin3:
Cola:1
Tea:1
Mustard:2
Pickles:2
Carrots:3
Bok Choy:3
Peaches:5
Melons:5
GroupJoin3: 8 items in 1 group Left Outer Join:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Nothing! 4
Group:
Peaches 5
Melons 5
LeftOuterJoin: 9 items in 5 groups LeftOuterJoin2: 9 items in 1 group
Left Outer Join 2:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Nothing! 4
Peaches 5
Melons 5
LeftOuterJoin2: 9 items in 1 group
Press any key to exit.
*/

let子句

在查询表达式中,存储子表达式的结果有时很有用,这样可以在随后的子句中使用。可以使用 let 关键字完成这一工作,该关键字可以创建一个新的范围变量,并且用您提供的表达式的结果初始化该变量。一旦用值初始化了该
范围变量,它就不能用于存储其他值。但如果该范围变量存储的是可查询的类型,则可以对其进行查询。
在下面的示例中,以两种方式使用了 let:
创建一个可以查询自身的可枚举类型。
使查询只能对范围变量 word 调用一次 ToLower。如果不使用 let,则必须在 where 子句的每个谓词中调用 ToLower。

class LetSample1
{
static void Main()
{
string[] strings =
{
"A penny saved is a penny earned.",
"The early bird catches the worm.",
"The pen is mightier than the sword."
}; // Split the sentence into an array of words
// and select those whose first letter is a vowel.
var earlyBirdQuery =
from sentence in strings
let words = sentence.Split(' ')
from word in words
let w = word.ToLower()
where w[] == 'a' || w[] == 'e'
|| w[] == 'i' || w[] == 'o'
|| w[] == 'u'
select word; // Execute the query.
foreach (var v in earlyBirdQuery)
{
Console.WriteLine("\"{0}\" starts with a vowel", v);
} // Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
"A" starts with a vowel
"is" starts with a vowel
"a" starts with a vowel
"earned." starts with a vowel
"early" starts with a vowel
"is" starts with a vowel
*/

ascending 上下文关键字用在查询表达式的 orderby 子句中,用于指定从最小到最大的排序顺序。因为 ascending 是默认排序顺序,所以您无须指定它。

descending 上下文关键字用在查询表达式的 orderby 子句中,用于指定从最大到最小的排序顺序。

on 上下文关键字在查询表达式的 join 子句中用于指定联接条件。

equals 上下文关键字用在查询表达式的 join 子句中,用于比较两个序列的元素。

by 上下文关键字用在查询表达式的 group 子句中,用于指定应该如何对返回的项进行分组。

in 上下文关键字可在下面三种上下文中使用:

  • foreach 语句
  • 查询表达式中的 join 子句
  • 泛型接口和委托中的泛型类型参数。

关键字:.NET,LINQ

[LINQ]查询关键字的更多相关文章

  1. 读书笔记 C# Linq查询之group关键字浅析

    在C#中,自从有了Linq查询表达式后,程序员对可被迭代的序列或列表执行一系列的筛选.排序.过滤.分组.查询等操作.本文章所要讲述的是group关键字. Linq查询表达式,是以from关键字开头,以 ...

  2. Linq查询基本操作

    摘要:本文介绍Linq查询基本操作(查询关键字) - from 子句 - where 子句 - select子句 - group 子句 - into 子句 - orderby 子句 - join 子句 ...

  3. C#基础:LINQ 查询函数整理

    1.LINQ 函数   1.1.查询结果过滤 :where() Enumerable.Where() 是LINQ 中使用最多的函数,大多数都要针对集合对象进行过滤,因此Where()在LINQ 的操作 ...

  4. Linq查询表达式

    目录 1. 概述 2. from子句 3. where子句 4. select子句 5. group子句 6. into子句 7. 排序子句 8. let子句 9. join子句 10. 小结 1. ...

  5. .NET LINQ查询操作中的类型关系

    LINQ 查询操作中的类型关系      若要有效编写查询,您应该了解完整的查询操作中的变量类型是如何全部彼此关联的. 如果您了解这些关系,就能够更容易地理解文档中的 LINQ 示例和代码示例. 另外 ...

  6. Linq中关键字的作用及用法

    Linq中关键字的作用及用法 1.All:确定序列中的所有元素是否都满足条件.如果源序列中的每个元素都通过指定谓词中的测试,或者序列为空,则为 true:否则为 false. Demo: 此示例使用 ...

  7. LINQ查询返回DataTable类型

    个人感觉Linq实用灵活性很大,参考一篇大牛的文章LINQ查询返回DataTable类型 http://xuzhihong1987.blog.163.com/blog/static/267315872 ...

  8. Linq查询

    //Linq查询 List<A1> a1 = new List<A1>(); a1.Add(, Name = , Gender = true }); a1.Add(, Name ...

  9. Silverlight项目笔记1:UI控件与布局、MVVM、数据绑定、await/async、Linq查询、WCF RIA Services、序列化、委托与事件

    最近从技术支持转到开发岗,做Silverlight部分的开发,用的Prism+MVVM,框架由同事搭好,目前做的主要是功能实现,用到了一些东西,侧重于如何使用,总结如下 1.UI控件与布局 常用的主要 ...

随机推荐

  1. MongoDB介绍及下载与安装

    MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的.他支持的数据结构非常松散,是类似json的bjson格式,因此可以存储比较复杂的数据类型.M ...

  2. UI进阶 数据处理之文件读写

    目录: 1-------沙盒机制(SandBox) 2-------简单对象的读写(I/O)操作 3-------复杂对象的读写(I/O)操作 一.沙盒机制(SandBox) 什么是沙盒:每个iOS应 ...

  3. Oracle 中 union与union all

    如果我们需要将两个select语句的结果作为一个整体显示出来,我们就需要用到union或者union all关键字. union(或称为联合)的作用是将多个结果合并在一起显示出来. union和uni ...

  4. H5页面请求跨域问题

    1.  <meta http-equiv="Access-Control-Allow-Origin" content="*"> 说明一下什么情况下我 ...

  5. ECSHOP在线手册布局参考图--商品详情页 goods.dwt

        A.购物车 1,设置方法 程序自动读取购物车的商品数量 2,代码相关 cart.lbi 中 {insert_scripts files='transport.js'} <div clas ...

  6. 判断一个Bitmap图像是否是.9图

    见BitmapFactory的源码中 byte[] np = bm.getNinePatchChunk();  final boolean isNinePatch = np != null & ...

  7. 从零开始学android开发-获取TextView的值

    昨日写一个Android Demo,逻辑大概是从TextView获取其中的值,然后处理后再放回TextView中.整个处理过程是由一个Button的OnClick触发的. 可是在调试的过程中,一点击B ...

  8. C#利用lambda表达式将函数作为参数或属性跨类传递

    在编码时,由于开始是在winform下进行简单的测试开发的,后来代码多了,就想分到不同的类里边去,可是因为原来的测试是在同一个form下的,所以对于函数调用可以很方便,而一旦跨类之后,就会发现,这函数 ...

  9. UVa 131 - The Psychic Poker Player

    题目:手里有五张牌,桌上有一堆牌(五张).你能够弃掉手中的k张牌,然后从牌堆中取最上面的k个. 比較规则例如以下:(按优先级排序) 1.straight-flush:同花顺.牌面为T(10) - A, ...

  10. 【源码分享】iOS-OC版五子棋

    五子棋是程序猿比较熟悉的一款小游戏,相信很多人大学时期就用多种语言写过五子棋小游戏,笔者工作闲暇之余,试着用OC实现了一下,在这里给大家分享一下.有不足之处,欢迎大家提供建议和指点! GitHub源码 ...