进阶系列(9)——linq
一、揭开linq的神秘面纱
(一)概述
LINQ的全称是Language Integrated Query,中文译成“语言集成查询”。LINQ作为一种查询技术,首先要解决数据源的封装,大致使用了三大组件来实现这个封装,分别是LINQ to Object、LINQ to ADO.NET、LINQ to XML。它们和.NET语言的关系如下

要使用LINQ来编程,首先要学习使用LINQ的子句以及由查询语法构成的查询表达式。C#3.0和VB9开始将这种查询语法引入到了编程语言,并新增了一系列的关键字。但对于CLR本身来说,它并不了解查询语法,它能理解的是由编程语言的编译器将这种查询语法转换成的方法。这些方法叫“标准查询运算符”,它们具有类似这样的名—Where、Select、GroupBy、Join。下面就以C#为例,从编程语言的层面来具体介绍这些查询语法(注意VB9也支持这种查询语法)。
LINQ的查询由3基本部分组成:获取数据源,创建查询,执行查询
// 1,获取数据源
List<int> numbers = new List<int>() { , , , , , , , , , }; // 2,创建查询
var numQuery = from num in numbers
where num % ==
select num;
// 3,执行查询
foreach (var num in numQuery)
{
Console.WriteLine("{0,1}", num);
}
下图显示了完整的查询操作。在 LINQ 中,查询的执行与查询本身截然不同;换句话说,如果只是创建查询变量,则不会检索任何数据。

如上例所示,Linq的数据源要求必须实现IEnumerable或IEnumerable<T>接口,数组隐式支持这个接口。numQuery叫做查询变量,它存储了一个查询表达式。注意,声明查询变量并不会执行查询,真正的执行查询延迟到了foreach语句中。
linq的机制用到的详细知识点请参考:https://www.cnblogs.com/liulun/archive/2013/02/26/2909985.html
二、linq常用方法小试牛刀
1. from子句
创建一个LINQ表达式必须要以from子句开头。
1.1 单个from子句
string[] values = { "中国", "日本", "美国", "菲律宾", "越南" };
//查询包含“国”的字符串
var valueQuery = from v in values
where v.IndexOf("国") > 0
select v;
foreach (var v in valueQuery)
{
Console.WriteLine("{0,1}", v);
}
在这个LINQ表达式的from子句中,v叫做范围变量,values是数据源。v的作用域存在于当前的LINQ表达式,表达式以外不能访问这个变量。where用来筛选元素,select用于输出元素。这里的范围变量v,和foreach语句中得隐式变量v都可以由编译器推断出其类型。
运行的结果如下:
中国
美国
使用LINQ查询List<T>集合
public class CustomerInfo
{
public string Name { get; set; }
public int Age { get; set; }
public string Tel { get; set; }
}
private void formExpDemo2()
{
//这里用了,对象和集合初始化器
List<CustomerInfo> customers = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="诸葛菲菲", Age=23, Tel ="1380524****"}
};
//查询年龄大于20的客户,注意这里的范围变量用了显示类型CustomerInfo
var query = from CustomerInfo ci in customers
where ci.Age > 20
select ci; foreach (CustomerInfo ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
}
结果:
姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:诸葛菲菲 年龄:23 电话:1380524****
1.2 复合from子句
在查询数据源中,元素的属性是一个集合时,可以使用复合from子句对这个属性集合查询。比如,一个客户,可能有多个电话。
public class CustomerInfo
{
public string Name { get; set; }
public int Age { get; set; }
public List<string> TelTable { get; set; }
}
private void formExpDemo()
{
List<CustomerInfo> customers = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, TelTable=new List<string>{"1330708****","1330709****"}},
new CustomerInfo{ Name="上官飘飘", Age=17, TelTable=new List<string>{"1592842****","1592843****"}},
new CustomerInfo{ Name="诸葛菲菲", Age=23, TelTable=new List<string>{"1380524****","1380525****"}}
};
//查询包含电话号码1592842****的客户
var query = from CustomerInfo ci in customers
from tel in ci.TelTable
where tel.IndexOf("1592842****") > -1
select ci; foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1}", ci.Name, ci.Age);
foreach (var tel in ci.TelTable)
{
Console.WriteLine(" 电话:{0}", tel);
}
}
}
结果:
姓名:上官飘飘 年龄:17
电话:1592842****
电话:1592843****
1.3 多个from子句
多个from子句查询和复合from子句从字面上看似乎一样,其实是不同的操作。复合from子句查询的是单个数据源中的子元素的集合,而多个from子句,是载入多个数据源进行查询。
private void formExpDemo()
{
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="诸葛菲菲", Age=23, Tel ="1380524****"}
};
List<CustomerInfo> clist2 = new List<CustomerInfo> {
new CustomerInfo{ Name="令狐冲", Age=25, Tel ="1330708****"},
new CustomerInfo{ Name="东方不败", Age=35, Tel ="1592842****"},
new CustomerInfo{ Name="任盈盈", Age=23, Tel ="1380524****"}
}; //在clist中查找Age大于20的客户,
//在clist2中查找Age小于30的客户
var query = from customer in clist
where customer.Age > 20
from customer2 in clist2
where customer2.Age < 30
select new { customer, customer2 }; foreach (var ci in query)
{
Console.WriteLine("{0} {1}", ci.customer.Name,ci.customer2.Name);
}
}
在select语句中,我们用了匿名类型来存储筛选出的元素,这样得到的完全是一个交叉联接表,有点类似于SQL中的笛卡尔乘积。
输出的结果:
欧阳晓晓 令狐冲
欧阳晓晓 任盈盈
诸葛菲菲 令狐冲
诸葛菲菲 任盈盈
2、where子句
where子句的作用就是筛选元素,除了开始和结束位置,where子句几乎可以出现在LINQ表达式的任意位置。一个LINQ表达式中可以有where子句,也可以没有;可以有一个,可以有多个;多个where子句之间的关系相当于逻辑“与”,每个where子句可以包含1个或多个逻辑表达式,这些条件成为“谓词”,多个谓词之间用布尔运算符隔开,比如逻辑“与”用&&,逻辑“或”用||,而不是用SQL中的AND或OR。
2.1 常见的where子句查询
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
};
//查询名字是3个字或者姓“令”的,但年龄大于20的客户
var query = from customer in clist
where (customer.Name.Length == 3 || customer.Name.Substring(0, 1) == "令")
&& customer.Age > 20
select customer;
foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
结果:
姓名:令狐冲 年龄:23 电话:1380524****
2.2 在where子句中使用自定义函数
private void whereExpDemo()
{
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
}; //查询名字是3个字并且姓“令”的客户
var query = from customer in clist
where (customer.Name.Length == 3 && CheckName(customer.Name))
select customer; foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
}
private bool CheckName(string name)
{
if (name.Substring(0, 1) == "令")
return true;
else
return false;
}
结果:
姓名:令狐冲 年龄:23 电话:1380524****
2.3 动态谓词的筛选
上面的几个例子都是给定了查询谓词然后进行查询,有时候谓词的数量可能并不固定,是随情况变化的。例如:一组名字可能是运行时动态指定的。
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
};
//定义动态的谓词数组,这个数组应该由实际运行环境生成
string[] names = { "令狐冲", "任盈盈", "杨过", "小龙女", "欧阳晓晓" };
//查询在给定谓词数组里存在的客户
var query = from customer in clist
where names.Contains(customer.Name)
select customer;
foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
结果:
姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:令狐冲 年龄:23 电话:1380524****
3. select子句
LINQ表达式的结果是使用select子句获得的。select子句可以对数据进行转换,这个过程称为“投影”。select子句产生的类容,取决于前面的所有子句及其自身表达式执行后的结果。
3.1 输出查询结果
最简单的select就是直接输出from子句建立的那个范围变量:
var query = from customer in clist
where names.Contains(customer.Name)
select customer;
也可以输出范围变量类型中得某个属性:
select customer.Name;
或者修改一下再输出:
select customer.Name.Replace("gg","mm");
或者干脆使用一个自定义的函数,把范围变量传进去,输出处理后的结果:
select MyFunction(customer.Name);
3.2 对查询结果进行投影
public class MyCustomerInfo
{
public string Name { get; set; }
public string Tel { get; set; }
}
private void whereExpDemo()
{
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
}; //定义动态的谓词数组,这个数组应该由实际运行环境生成
string[] names = { "令狐冲", "任盈盈", "杨过", "小龙女", "欧阳晓晓" }; //查询在给定谓词数组里存在的客户
var query = from customer in clist
where customer.Age < 30
select new MyCustomerInfo { Name = customer.Name, Tel = customer.Tel }; foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 电话:{1} 类型{2}", ci.Name, ci.Tel,ci.GetType().FullName);
}
}
上例中,在select子句中用对象初始化器生成了新的数据类型,从而进行了数据转换,使元素变成了MyCustomerInfo类型。
姓名:上官飘飘 电话:1592842**** 类型LinqDemo.Form1+MyCustomerInfo
姓名:令狐冲 电话:1380524**** 类型LinqDemo.Form1+MyCustomerInfo
4. group子句
按照语法的规定,LINQ表达式必须以from子句开头,以select或group子句结束,所以除了使用select子句外,也可以使用guoup子句来返回元素分组后的结果。group子句返回的是一个IGrouping<TKey,TElement>泛型接口的对象集合,下面先了解下这个接口。
4.1 IGrouping<TKey,TElement>泛型接口
这个接口表示具有公共键的对象集合,它的原型如下:
public interface IGrouping<TKey, TElement> : IEnumerable<TElement>,
IEnumerable
TKey是键的对象类型,在用于group子句的时候,数据类型会有编译器推断出来,它一般用于存储分组的键值;TElement是指的对象类型,用于存储分组的结果,变量基于这个接口的类型就是遍历这个值。
4.2 分组查询
分组查询对于关系型数据库是非常常见的一种操作,但在没有LINQ之前,对内存的对象进行分组却是一件非常麻烦的事情。现在,在LINQ表达式中只需要使用group子句就可以轻松完成对内存对象的分组。
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"}
};
//按照名字的前2个字进行分组
var query = from customer in clist
group customer by customer.Name.Substring(0, 2);
foreach (IGrouping<string,CustomerInfo> group in query)
{
Console.WriteLine("分组键:{0}",group.Key);
foreach (var ci in group)
{
Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel);
}
Console.WriteLine("***************************************");
}
上例代码,按照form子句建立的范围变量customer的Name属性的前两个字作为键值进行分组。所以TKey的类型是一个字符串类型。
分组键:欧阳
姓名:欧阳晓晓 电话:1330708****
姓名:欧阳锦鹏 电话:1330708****
***************************************
分组键:上官
姓名:上官飘飘 电话:1592842****
姓名:上官无忌 电话:1380524****
***************************************
再看一个分组的例子:
//按照年龄是否大于20分组
var query = from customer in clist
group customer by customer.Age > 20; foreach (var group in query)
{
Console.WriteLine("分组键:{0}",group.Key);
foreach (var ci in group)
{
Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel);
}
Console.WriteLine("***************************************");
}
group子句用了一个布尔表达式,所以IGrouping<TKey,TElement>的TKey变成了一个bool型。并且循环遍历的时候可以用var代替IGrouping的声明:
foreach (var group in query)
分组键:True
姓名:欧阳晓晓 电话:1330708****
姓名:欧阳锦鹏 电话:1330708****
姓名:上官无忌 电话:1380524****
***************************************
分组键:False
姓名:上官飘飘 电话:1592842****
***************************************
5. into子句
into子句作为一个临时标识符,用于select,group,join子句中。
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"}
};
//按照名字的前两个字进行分组,再用分组Key进行排序
var query = from customer in clist
group customer by customer.Name.Substring(0, 2) into gpcustomer
orderby gpcustomer.Key descending
select gpcustomer;
Console.WriteLine("into 用于group子句");
foreach (var group in query)
{
Console.WriteLine("分组键:{0}", group.Key);
foreach (var ci in group)
{
Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel);
}
Console.WriteLine("***************************************");
}
var query2 = from customer in clist
select new { NewName = customer.Name, NewAge = customer.Age } into newCustomer
orderby newCustomer.NewAge
select newCustomer;
Console.WriteLine("into 用于select子句");
foreach (var ci in query2)
{
Console.WriteLine("{0} 年龄:{1}", ci.NewName, ci.NewAge);
}
into子句提供了一个临时标识符,它存储了into子句前面的查询内容,使它后面的子句可以方便的使用,对其进行再次查询,投影等操作。
执行结果:
into 用于group子句
分组键:上官
姓名:上官飘飘 电话:1592842****
姓名:上官无忌 电话:1380524****
***************************************
分组键:欧阳
姓名:欧阳晓晓 电话:1330708****
姓名:欧阳锦鹏 电话:1330708****
***************************************
into 用于select子句
上官飘飘 年龄:17
上官无忌 年龄:23
欧阳晓晓 年龄:35
欧阳锦鹏 年龄:
6. 排序子句
LINQ可以按元素的一个或多个属性对元素进行排序。LINQ表达式的排序方式分为OrderBy、OrderByDescending、ThenBy、ThenByDescending这四种。
6.1 OrderBy和OrderByDescending
OrderBy用于按元素的值进行升序,语法:
orderby 用于排序的元素的表达式
OrderByDescending用于按元素的值进行降序,语法:
orderby 用于排序的元素的表达式 descending
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"}
};
//按照年龄升序
var query = from customer in clist
orderby customer.Age
select customer;
Console.WriteLine("按年龄升序排列");
foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
//按照年龄降序
var query2 = from customer in clist
orderby customer.Age descending
select customer;
Console.WriteLine("\n按年龄降序排列");
foreach (var ci in query2)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
运行结果:
按年龄升序排列
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:上官无忌 年龄:23 电话:1380524****
姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:欧阳锦鹏 年龄:35 电话:1330708**** 按年龄降序排列
姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:欧阳锦鹏 年龄:35 电话:1330708****
姓名:上官无忌 年龄:23 电话:1380524****
姓名:上官飘飘 年龄:17 电话:1592842****
6.2 ThenBy和ThenByDescending
ThenBy和ThenByDescending用于对元素进行次要排序。基本语法:
orderby 用于排序的元素表达式,用于排序的元素表达式
orderby 用于排序的元素表达式,用于排序的元素表达式 descending
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"},
new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"}
};
//按照年龄升序,再按名字的字数次要排序
var query = from customer in clist
orderby customer.Age,customer.Name.Length
select customer;
Console.WriteLine("按年龄排列,按名字字数进行次要排序");
foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
//按照年龄升序,再按名字的字数降序次要排序
var query2 = from customer in clist
orderby customer.Age, customer.Name.Length descending
select customer;
Console.WriteLine("\n按年龄排列,按名字字数进行降序次要排序");
foreach (var ci in query2)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
//按照年龄升序,再按名字的字数降序要排序,在按电话号码进行第三条件排序
var query3 = from customer in clist
orderby customer.Age, customer.Name.Length,customer.Tel
select customer;
Console.WriteLine("\n按年龄,名字字数,电话号码排序");
foreach (var ci in query3)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
执行结果:
按年龄排列,按名字字数进行次要排序
姓名:郭靖 年龄:17 电话:1330708****
姓名:黄蓉 年龄:17 电话:1300524****
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:欧阳晓晓 年龄:35 电话:1330708**** 按年龄排列,按名字字数进行降序次要排序
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:郭靖 年龄:17 电话:1330708****
姓名:黄蓉 年龄:17 电话:1300524****
姓名:欧阳晓晓 年龄:35 电话:1330708**** 按年龄,名字字数,电话号码排序
姓名:黄蓉 年龄:17 电话:1300524****
姓名:郭靖 年龄:17 电话:1330708****
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:欧阳晓晓 年龄:35 电话:1330708****
7. let子句
let子句用于在LINQ表达式中存储子表达式的计算结果。let子句创建一个范围变量来存储结果,变量被创建后,不能修改或把其他表达式的结果重新赋值给它。此范围变量可以再后续的LINQ子句中使用。
List<CustomerInfo> clist = new List<CustomerInfo> {
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"},
new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"}
};
//姓“郭”或“黄”的客户
var query = from customer in clist
let g = customer.Name.Substring(0,1)
where g == "郭" || g == "黄"
select customer;
foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
}
使用let 建立了个范围变量,这个范围变量在后续的where子句中使用,如果不使用let子句,where子句的表达式将写成这样:
where customer.Name.Substring(0, 1) == "郭" || customer.Name.Substring(0, 1) == "黄"
姓名:郭靖 年龄:17 电话:1330708****
姓名:黄蓉 年龄:17 电话:1300524****
8. join子句
如果一个数据源中元素的某个属性可以跟另一个数据源中元素的属性进行相等比较,那么这两个数据源可以用join子句进行关联。jion子句用equals关键字进行比较,而不是常见的==。
List<CustomerInfo> clist = new List<CustomerInfo>
{
new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"},
new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"}
}; List<CustomerTitle> titleList = new List<CustomerTitle>
{
new CustomerTitle{ Name="欧阳晓晓", Title="歌手"},
new CustomerTitle{ Name="郭靖", Title="大侠"},
new CustomerTitle{ Name="郭靖", Title="洪七公徒弟"},
new CustomerTitle{ Name="黄蓉", Title="才女"},
new CustomerTitle{ Name="黄蓉", Title="丐帮帮主"}
}; //根据姓名进行内部联接
Console.WriteLine("内部联接");
var query = from customer in clist
join title in titleList
on customer.Name equals title.Name
select new { Name = customer.Name, Age = customer.Age, Title = title.Title };
foreach (var ci in query)
{
Console.WriteLine("姓名:{0} 年龄:{1} {2}", ci.Name, ci.Age, ci.Title);
}
//根据姓名进行分组联接
Console.WriteLine("\n根据姓名进行分组联接");
var query2 = from customer in clist
join title in titleList
on customer.Name equals title.Name into tgroup
select new { Name = customer.Name, Titles = tgroup };
foreach (var g in query2)
{
Console.WriteLine(g.Name);
foreach (var g2 in g.Titles)
{
Console.WriteLine(" {0}", g2.Title);
}
}
//根据姓名进行 左外部联接
Console.WriteLine("\n左外部联接");
var query3 = from customer in clist
join title in titleList
on customer.Name equals title.Name into tgroup
from subTitle in tgroup.DefaultIfEmpty()
select new { Name = customer.Name, Title = (subTitle == null ? "空缺" : subTitle.Title) };
foreach (var ci in query3)
{
Console.WriteLine("姓名:{0} {1} ", ci.Name, ci.Title);
}
要仔细理解上例的,内联接,分组联接,以及左联接。
内部联接
姓名:欧阳晓晓 年龄: 歌手
姓名:郭靖 年龄: 大侠
姓名:郭靖 年龄: 洪七公徒弟
姓名:黄蓉 年龄: 才女
姓名:黄蓉 年龄: 丐帮帮主 根据姓名进行分组联接
欧阳晓晓
歌手
上官飘飘
郭靖
大侠
洪七公徒弟
黄蓉
才女
丐帮帮主 左外部联接
姓名:欧阳晓晓 歌手
姓名:上官飘飘 空缺
姓名:郭靖 大侠
姓名:郭靖 洪七公徒弟
姓名:黄蓉 才女
姓名:黄蓉 丐帮帮主
三、Linq to Objects之延期执行方法
LINQ to Objects是 LINQ的基础,而 LINQ to SQL、 LINQ to XML是中间 LINQ提供程序,他们主要是把数据源转换成 LINQ to Objects兼容的类型,以便 LINQ to Objects进行操作。 LINQ to Objects就是直接对IEnumerable或泛型IEnumerable<T>集合进行查询。LINQ表达式是LINQ标准查询运算符的一部分,而LINQ标准查询运算符则是LINQ to Objects的基础。它们是一组静态方法,被定义在System.Linq.Enumerable和System.Linq.Queryable类中。这两个类中方法基本一致,唯一的不同点是System.Linq.Queryable类中方法会把LINQ表达式拆解成表达式目录树,其他一些Linq提供程序可以将这个表达式目录树翻译成查询语句,比如SQL语句,然后再执行相关操作。
本文主要学习System.Linq.Enumerable的扩展方法,这些方法按照执行的行为不同,可以分为延期执行和立即执行。延期执行的运算符在枚举时被执行,下面要学习的就是延期执行方法的一部分。
1,Take 方法
Take方法用于从一个序列的开头返回指定数量的元素。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
//直接输出前3个元素
Console.WriteLine("Take方法直接输出前3个元素");
foreach (var name in names.Take(3))
{
Console.WriteLine(name);
}
var query = from n in names
where n.Length == 2
select n;
Console.WriteLine("\nTake方法输出查询结果的前1个元素");
foreach (var s in query.Take(1))
{
Console.WriteLine(s);
}
输出结果:
Take方法直接输出前3个元素
郭靖
李莫愁
欧阳晓晓 Take方法输出查询结果的前1个元素
郭靖
2,TakeWhile 方法
TakeWhile方法获取序列中从开头起符合条件的元素,直到遇到不符合条件的元素为止的所有元素。条件代理部分有两种形式:
Func<TSource, bool> predicate
Func<TSource, int, bool> predicate 第二个参数是元素的索引
注意:当条件为假时,就停止了,后面的元素不会输出。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
//输出名字小于4个字的元素
var takeNames = names.TakeWhile(n => n.Length < 4);
foreach (var name in takeNames)
{
Console.WriteLine(name);
}
Console.WriteLine("\nTakeWhile 带索引参数");
//输出名字字数小于等于4 并且索引小于4的元素
foreach (var name in names.TakeWhile((n, i) => n.Length <= 4 && i < 4))
{
Console.WriteLine(name);
}
输出结果:
郭靖
李莫愁 TakeWhile 带索引参数
郭靖
李莫愁
欧阳晓晓
黄蓉
3,Skip 方法
Skip方法用于跳过序列中指定个数的元素。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
//跳过前3个元素
Console.WriteLine("Take方法跳过前3个元素");
foreach (var name in names.Skip(3))
{
Console.WriteLine(name);
}
var query = from n in names
where n.Length == 2
select n;
Console.WriteLine("\nTake方法跳过查询结果的前1个元素");
foreach (var s in query.Skip(1))
{
Console.WriteLine(s);
}
输出结果:
Take方法跳过前3个元素
黄蓉
黄药师 Take方法跳过查询结果的前1个元素
黄蓉
4,SkipWhile 方法
SkipWhile 方法用于只要满足指定的条件,就跳过序列中得元素。
注意:当遇到条件为假时,就停止跳越了,输出剩余的所有元素。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
Console.WriteLine("SkipWhile跳过名字为2个字的元素");
foreach (var name in names.SkipWhile(n => n.Length == 2))
{
Console.WriteLine(name);
}
Console.WriteLine("\nSkipWhile跳过名字小于4个字,并且索引小于2");
foreach (var s in names.SkipWhile((n, i) => n.Length < 4 && i < 2))
{
Console.WriteLine(s);
}
输出结果:
SkipWhile跳过名字为2个字的元素
李莫愁
欧阳晓晓
黄蓉
黄药师 SkipWhile跳过名字小于4个字,并且索引小于2
欧阳晓晓
黄蓉
黄药师
5,Reverse 方法
Reverse 方法用于反转序列中的元素。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
foreach (var name in names.Reverse())
{
Console.WriteLine(name);
}
输出结果:
黄药师
黄蓉
欧阳晓晓
李莫愁
郭靖
6,Distinct 方法
Distinct 方法用于去除重复元素。
string[] names = { "郭靖", "郭靖", "李莫愁", "欧阳晓晓", "欧阳晓晓", "黄蓉", "黄药师" };
Console.WriteLine("含有重复元素的数组");
foreach (var name in names)
{
Console.Write(name + " ");
}
Console.WriteLine("\n\n去除重复元素的数组");
foreach (var name in names.Distinct())
{
Console.Write(name + " ");
}
输出结果:
含有重复元素的数组
郭靖 郭靖 李莫愁 欧阳晓晓 欧阳晓晓 黄蓉 黄药师 去除重复元素的数组
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师
自定义IEqualityComparer<T>接口的相等比较器
public class MyEqualityComparer<T> : IEqualityComparer<T>
{
#region IEqualityComparer<T> 成员 public bool Equals(T x, T y)
{
string temp = x as string;
if (temp != null)
{
if (temp == "欧阳晓晓") //对"欧阳晓晓"不过滤
return false;
}
if (x.GetHashCode() == y.GetHashCode())
return true;
else
return false;
} public int GetHashCode(T obj)
{
return obj.GetHashCode();
} #endregion
}
private void DistinctDemo()
{
string[] names = { "郭靖", "郭靖", "李莫愁", "欧阳晓晓", "欧阳晓晓", "黄蓉", "黄药师" }; Console.WriteLine("含有重复元素的数组");
foreach (var name in names)
{
Console.Write(name + " ");
}
Console.WriteLine("\n\n去除重复元素的数组,实现自定义IEqualityComparer<T>");
foreach (var name in names.Distinct(new MyEqualityComparer<string>()))
{
Console.Write(name + " ");
}
}
输出结果:
含有重复元素的数组
郭靖 郭靖 李莫愁 欧阳晓晓 欧阳晓晓 黄蓉 黄药师 去除重复元素的数组,实现自定义IEqualityComparer<T>
郭靖 李莫愁 欧阳晓晓 欧阳晓晓 黄蓉 黄药师
7,Union 方法
Union 方法 用于合并两个序列,并去掉重复元素。
string[] names = { "郭靖", "郭靖", "李莫愁", "欧阳晓晓", "欧阳晓晓", "黄蓉", "黄药师" };
Console.WriteLine("含有重复元素的数组");
foreach (var name in names)
{
Console.Write(name + " ");
}
Console.WriteLine("\n\n去除重复元素的数组,实现自定义IEqualityComparer<T>");
foreach (var name in names.Distinct(new MyEqualityComparer<string>()))
{
Console.Write(name + " ");
}
输出结果:
合并后的元素
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师 杨过
自定义IEqualityComparer<T>接口的相等比较器
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };
Console.WriteLine("合并后的元素");
foreach (var name in names.Union(names2,new MyEqualityComparer<string>()))
{
Console.Write(name + " ");
}
输出结果:
合并后的元素
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师 杨过 欧阳晓晓
8,Concat 方法
Concat 方法 用于连接两个序列,与Union不同,它不会过滤重复的元素。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };
Console.WriteLine("合并后的元素");
foreach (var name in names.Concat(names2))
{
Console.Write(name + " ");
}
输出元素:
合并后的元素
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师 郭靖 杨过 欧阳晓晓
9,Intersect 方法
Intersect 方法用于生成两个序列的交集。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };
Console.WriteLine("相交的元素");
foreach (var name in names.Intersect(names2))
{
Console.Write(name + " ");
}
输出结果:
相交的元素
郭靖 欧阳晓晓
自定义IEqualityComparer<T>
public class MyEqualityComparer<T> : IEqualityComparer<T>
{
#region IEqualityComparer<T> 成员 public bool Equals(T x, T y)
{
string temp = x as string;
if (temp != null)
{
if (temp == "欧阳晓晓") //对"欧阳晓晓"不过滤
return false;
}
if (x.GetHashCode() == y.GetHashCode())
return true;
else
return false;
} public int GetHashCode(T obj)
{
return obj.GetHashCode();
} #endregion
}
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };
Console.WriteLine("相交的元素");
foreach (var name in names.Intersect(names2,new MyEqualityComparer<string>()))
{
Console.Write(name + " ");
}
输出结果:
相交的元素
郭靖
10,Except 方法
Except 方法用于生成两个序列的差集。
注意:返回是第一个数组里,去掉指定数组里的元素后,剩下的一个序列。
它和Intersect方法不是互补的,不要搞混了。下面的“杨过”就不会输出。因为它是指定数组里的元素,和源数组一毛钱关系都没有。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };
Console.WriteLine("2个数组的不同元素");
foreach (var name in names.Except(names2))
{
Console.Write(name + " ");
}
输出结果:
2个数组的不同元素
李莫愁 黄蓉 黄药师
运用自定义IEqualityComparer<T>指定比较器。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };
Console.WriteLine("2个数组的不同元素");
foreach (var name in names2.Except(names,new MyEqualityComparer<string>()))
{
Console.Write(name + " ");
}
输出结果:
2个数组的不同元素
杨过 欧阳晓晓
11,Range 方法
Range 方法用于生成指定范围的整数序列。在BS程序中,经常需要分页显示,在页面中需要显示页面号码的链接,用这个方法可以生成页码的数组。
由于没有this关键字,它是一个普通的静态方法。
int istart = 1;//起始页码
int iend = 12; //结束页码 var pages = Enumerable.Range(1, iend - istart + 1);
Console.WriteLine("输出页码");
foreach (var n in pages)
{
Console.Write("[{0}] ", n);
}
输出结果:
输出页码
[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12]
12,Repeat 方法
Repeat 方法用于生成指定数量重复元素的序列。由于没有this关键字,它是一个普通的静态方法。
var people = new { Name = "郭靖", Age = 35 };//定义一个匿名类型
var peoples = Enumerable.Repeat(people, 4);
Console.WriteLine("包含4个匿名元素:");
foreach (var n in peoples)
{
Console.WriteLine("{0} {1} ", n.Name, n.Age);
}
输出结果:
包含4个匿名元素:
郭靖 35
郭靖 35
郭靖 35
郭靖 35
13,Empty 方法
Empty 方法用于获取一个指定类型参数的空序列。由于没有this关键字,它是一个普通的静态方法。
var s = Enumerable.Empty<string>();
Console.WriteLine("序列的元素数:{0} ", s.Count());
输出结果:
序列的元素数:0
14,DefaultIfEmpty 方法
DefaultIfEmpty 方法用于获取序列,如果序列为空则添加一个类型的默认值。例如:如果元素为引用类型,添加null元素;元素为int类型,则添加int的默认值0。
string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
var intempty = Enumerable.Empty<int>();//空的Int类型序列
//没有找到元素的序列
var empty = from n in names
where n.Length == 5
select n;
Console.WriteLine("DefaultIfEmpty 返回有内容的序列");
foreach (var n in names)
{
Console.Write("{0} ", n);
}
Console.WriteLine("\nempty空序列元素数:{0}", empty.Count());
Console.WriteLine("empty空序列应用DefaultIfEmpty 后的元素数:{0}", empty.DefaultIfEmpty().Count());
Console.Write("empty空序列应用DefaultIfEmpty 后的元素值:");
foreach (var n in empty.DefaultIfEmpty())
{
if (n == null)
Console.Write("null");
}
Console.WriteLine("\n****************************************");
Console.WriteLine("intempty空序列元素数:{0}", intempty.Count());
Console.WriteLine("intempty空序列应用DefaultIfEmpty 后的元素数:{0}", intempty.DefaultIfEmpty().Count());
Console.Write("intempty空序列应用DefaultIfEmpty 后的元素值:");
foreach (var n in intempty.DefaultIfEmpty())
{
Console.Write(n);
}
输出结果:
DefaultIfEmpty 返回有内容的序列
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师
empty空序列元素数:0
empty空序列应用DefaultIfEmpty 后的元素数:1
empty空序列应用DefaultIfEmpty 后的元素值:null
****************************************
intempty空序列元素数:0
intempty空序列应用DefaultIfEmpty 后的元素数:1
intempty空序列应用DefaultIfEmpty 后的元素值:
这个方法还可以指定一个自定义的默认值。
var intempty = Enumerable.Empty<int>();//空的Int类型序列
Console.Write("int 类型自定义默认值:");
foreach (var i in intempty.DefaultIfEmpty(200))
{
Console.Write(i);
}
输出结果:
int 类型自定义默认值:
7、Cast 方法
Cast 方法用于按照TResult类型转换IEnumerable序列的集合。
//ArrayList没有实现IEnumerable<T>接口
ArrayList names = new ArrayList();
names.Add("郭靖");
names.Add("李莫愁");
names.Add("欧阳晓晓"); IEnumerable<string> newNames = names.Cast<string>();
foreach (var s in newNames)
{
Console.WriteLine(s);
}
输出结果:
郭靖
李莫愁
欧阳晓晓
8,OfType 方法
OfType 方法用于根据TResult类型筛选IEnumerable类型序列的元素。它的用途和Cast方法类似,但OfType方法如果遇到不能强制转换成TResutl的类型,会丢弃该元素,而不会出现运行错误。
//ArrayList没有实现IEnumerable<T>接口
ArrayList names = new ArrayList();
names.Add("郭靖");
names.Add("李莫愁");
names.Add(100);
names.Add(new Stack());
names.Add("欧阳晓晓"); IEnumerable<string> newNames = names.OfType<string>();
foreach (var s in newNames)
{
Console.WriteLine(s);
}
输出结果:
郭靖
李莫愁
欧阳晓晓
9,AsEnumerable方法
AsEnumerable方法根据元素的类型转换为泛型IEnumerable<T>类型。
MSDN上的一个例子,AsEnumerable用于隐藏自己定义的和IEnumerable里的扩展方法同名的方法。
public class MyList<T> : List<T>
{
public IEnumerable<T> Where(Func<T, bool> predicate)
{
Console.WriteLine("In MyList of Where");
return Enumerable.Where(this, predicate);
}
}
private void AsEnumerableDemo()
{
MyList<string> list = new MyList<string>() { "郭靖", "黄蓉", "黄药师" }; var query1 = list.Where(n => n.Contains("郭"));
Console.WriteLine("query1 created"); var query2 = list.AsEnumerable().Where(n => n.Contains("郭"));
Console.WriteLine("query2 created");
}
运行结果:
In MyList of Where
query1 created
query2 created
AsEnumerable方法经常用于Linq To SQL查询,将IQueryable<T>转换成IEnumerable<T>接口。因为一些扩展方法,如Reverse等,虽然在IQueryable<T>里有定义,但并不能将其翻译成对应的SQL语句,所以运行时会报错。用AsEnumerable方法转换成IEnumerable<T>后,实际的数据就在内存中操作。关于IQueryable<T>调用AsEnumerable背后的转换本质,有待进一步考证。
四、 IEnumberable & IQueryable 区别
在应用到IEnumberable 和IQueryable两个接口时,代码往往很相似,从而造成了很多困惑,然后事实上他们两是有很大的区别的,各种都有自己特定的使用场景。下面是IEnumberable和IQueryable的属性对比:
| IEnumerable | IQueryable | |
| Namespace | System.Collections Namespace | System.Linq Namespace |
| 继承于 | No base interface | 继承于 IEnumerable |
| Deferred Execution | 支持 | 支持 |
| Lazy Loading | 不支持 | 支持 |
| 如何工作 | 当从数据库中查询数据,IEnumberable在服务器端执行查询操作,下载数据到客户端的内存中,然后再筛选数据,因此这个操作需要更多的工作而变得缓慢。 | 当从数据库中查询数据,IQueryable在服务器端根据所有的filter条件执行查询操作,因此该操作需要更少的工作而运行快。 |
| 适用于 | LINQ to Object and LINQ to XML queries. | LINQ to SQL queries. |
| 自定义查询 | 不支持 | 支持使用CreateQuery 和Execute 方法。 |
| Extension mehtod parameter |
Extension methods supported in IEnumerable takes functional objects. |
Extension methods supported in IEnumerable takes expression objects i.e. expression tree. |
| 使用场合 | 当从内存中的数据集合(如LIst,Array etc)查询数据的时候 | 当查询非内存中的数据集合(如远程数据库,service等)时。 |
| 最常使用 | 内存遍历 | Paging |
参考资料:http://www.cnblogs.com/xiashengwang/archive/2012/07/29/2613940.html
进阶系列(9)——linq的更多相关文章
- C#进阶系列——一步一步封装自己的HtmlHelper组件:BootstrapHelper(三:附源码)
前言:之前的两篇封装了一些基础的表单组件,这篇继续来封装几个基于bootstrap的其他组件.和上篇不同的是,这篇的有几个组件需要某些js文件的支持. 本文原创地址:http://www.cnblog ...
- C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下)
前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...
- C#进阶系列——DDD领域驱动设计初探(七):Web层的搭建
前言:好久没更新博客了,每天被该死的业务缠身,今天正好一个模块完成了,继续来完善我们的代码.之前的六篇完成了领域层.应用层.以及基础结构层的部分代码,这篇打算搭建下UI层的代码. DDD领域驱动设计初 ...
- DotNet进阶系列
一. 回顾历史 回顾个人发展历程,自2012年初次接触开发至今(2018年)已经有六个年头,这期间陆陆续续学习并掌握了不少技术,C#语言.ORM框架.多线程技术.设计模式.前端技术.MVC.MVVM框 ...
- Ext JS学习第十六天 事件机制event(一) DotNet进阶系列(持续更新) 第一节:.Net版基于WebSocket的聊天室样例 第十五节:深入理解async和await的作用及各种适用场景和用法 第十五节:深入理解async和await的作用及各种适用场景和用法 前端自动化准备和详细配置(NVM、NPM/CNPM、NodeJs、NRM、WebPack、Gulp/Grunt、G
code&monkey Ext JS学习第十六天 事件机制event(一) 此文用来记录学习笔记: 休息了好几天,从今天开始继续保持更新,鞭策自己学习 今天我们来说一说什么是事件,对于事件 ...
- C#进阶系列——WebApi 接口返回值不困惑:返回值类型详解
前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧.之前分享过一篇 C#进阶系列——WebApi接口传参不再困惑:传参详解 ...
- C#进阶系列——WebApi 接口参数不再困惑:传参详解
前言:还记得刚使用WebApi那会儿,被它的传参机制折腾了好久,查阅了半天资料.如今,使用WebApi也有段时间了,今天就记录下API接口传参的一些方式方法,算是一个笔记,也希望能帮初学者少走弯路.本 ...
- C#进阶系列——WebApi 接口测试工具:WebApiTestClient
前言:这两天在整WebApi的服务,由于调用方是Android客户端,Android开发人员也不懂C#语法,API里面的接口也不能直接给他们看,没办法,只有整个详细一点的文档呗.由于接口个数有点多,每 ...
- C#进阶系列——WebApi 跨域问题解决方案:CORS
前言:上篇总结了下WebApi的接口测试工具的使用,这篇接着来看看WebAPI的另一个常见问题:跨域问题.本篇主要从实例的角度分享下CORS解决跨域问题一些细节. WebApi系列文章 C#进阶系列— ...
- C#进阶系列——WebApi 身份认证解决方案:Basic基础认证
前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想 ...
随机推荐
- svg路径动画心得
svg动画,随着路线运动,项目中需要用到,接触的时候感觉很高级,但是不会-无从下手呀!于是在网上找相关资料,先借鉴再修改成自己的. <svg width="500" heig ...
- redis常用数据类型操作命令集锦
redis操作命令集锦 redis中五种数据类型 1) 字符串 String 特点: 存储所有的字符和字符串 应用场景: 做缓存使用 2) 哈希 hash 特点: 相当于java中hashMap集合 ...
- Ajax的跨域请求——JSONP的使用
一.什么叫跨域 域当然是别的服务器 (说白点就是去别服务器上取东西) 只要协议.域名.端口有任何一个不同,都被当作是不同的域. 总而言之,同源策略规定,浏览器的ajax只能访问跟它的HTML页面同源( ...
- 20155204 2016-2017-2 《Java程序设计》第2周学习总结
20155204 2016-2017-2 <Java程序设计>第2周学习总结 教材学习内容总结 本章主要学习了Java语言的基础语法,基本同C语言逻辑相通,比较着学不算难理解,包括了一些简 ...
- 【Todo】找出共同好友 & Spark & Hadoop面试题
找了这篇文章看了一下面试题<Spark 和hadoop的一些面试题(准备)> http://blog.csdn.net/qiezikuaichuan/article/details/515 ...
- RegExp,实现匹配合法邮箱(英文邮箱)的正则表达式
邮箱列表:@qq.com.@vip.qq.com.@foxmail.com,数字邮箱暂时不考虑 以下邮箱列表用于测试: lihaha@qq.com lihaha@vip.qq.com lihaha@f ...
- 19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...
- idea开启jquery提示及如何找到学习目标
idea开启jquery提示 根据这些library就知道该学习哪些技术了
- javaweb(三十一)——国际化(i18n)
一.国际化开发概述 软件的国际化:软件开发时,要使它能同时应对世界不同地区和国家的访问,并针对不同地区和国家的访问,提供相应的.符合来访者阅读习惯的页面或数据. 国际化(internationaliz ...
- 自己动手做AI:Google AIY开发工具包解析
2018年国际消费性电子展(CES)上,最明显的一个趋势是Amazon与Google的语音技术进驻战,如AmazonAlexa进驻到Acer笔电内,Google Assist进驻到KIA汽车内,其他如 ...