C# :IQueryable & IEnumerable
1. IEnumerable
namespace System.Collections:
public interface IEnumerable
{
public IEnumerator GetEnumerator ();
}
public interface IEnumerator
{
pubilc object Current { get; }
public bool MoveNext ();
public void Reset ();
}
IEnumerable 只有一个方法 GetEnumerator(), 既实现IEnumerable的所有泛型集合,都具备可枚举(IEnumerator)的能力。
IEnumerator 只有一个属性 Current,和两个方法 MoveNext() \ Reset()。
通过 MoveNext() 和 Current 可以不停地移动 enumerator 的位置并返回当前的元素。
Reset() 会将 enumerator 设置到初始位置,既第一个元素之前。
2. IQueryable
namespace System.Linq:
public interface IQueryable<out T> : System.Collections.Generic.IEnumerable<out T>, System.Linq.IQueryable {}
public interface IQueryable : System.Collections.IEnumerable
{
// 表达式树返回结果的元素类型
public Type ElementType { get; }
// 获取IQueryable实例的表达式树
public System.Linq.Expressions.Expression Expression { get; }
public System.Linq.IQueryProvider Provider { get; }
}
// 定义用于创建和执行IQueryable对象所描述的查询的方法
public interface IQueryProvider
{
public System.Linq.IQueryable CreateQuery(System.Linq.Expressions.Expression expression);
public System.Linq.IQueryable<TElement> CreateQuery<TElement> (System.Linq.Expressions.Expression expression);
public object? Execute(System.Linq.Expressions.Expression expression);
public TResult Execute<TResult> (System.Linq.Expressions.Expression expression);
}
IQueryable 继承 IEnumerable,所以 IQueryable 具备可枚举的能力。
IQueryable 中的 Expession / Provider 则用来实现LINQ to SQL,具体可以看接下来的2节详细解释。
3. LINQ to SQL
LINQ to SQL是.Net Framework v3.5的组件,是能够提供将关系数据作为对象管理的运行时基础结构。
以往,编程语言通过 API 访问数据库数据的时候,需要将查询语句转为文本字符串。LINQ to SQL则会将对象模型中的语言集成查询转换为SQL,并发给数据库执行。当返回结果时,LINQ to SQL会再转换回可以用编程语言处理额对象。
所以,当拥有一个查询的时候,并不意味着查询已经执行:
var q = from c in dbContext.Customers Where c.City == ":London" select c;
命令对象会保留描述查询的字符串,IQueryable 对象的 Expression。命令对象的 ExecuteReader() 方法执行后,以 DataReader 形式返回结果。IQueryable 对象通过 GetEnumerator() 方法返回 IEnumerator
结果。
如下 foreach 会执行两次 query,这种行为称为延迟执行:
var q =
from c in db.Customers
where c.City == "London"
select c;
// Execute first time
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
// Execute second time
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
如果提前将结果转为任意标准的集合类,可以避免重复执行。
var q =
from c in db.Customers
where c.City == "London"
select c;
// Execute once using ToList() or ToArray()
var list = q.ToList();
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
更多资料:LINQ:.NET Language-Integrated查询
4. IEnumerable & IQueryable
4.1 Expression
针对 IEnumerable
IQueryable 会解析表达式树,并把这棵树表示的逻辑转为 provider 能够操作的格式,将其放在离数据最近的地方去执行。即传输数据往往会少于 IEnumerable,总体性能更好。
借鉴 《Effective C#》中的例子,如下两种写法,返回的结果相同,但是工作方式却不同:
// 1. use IQueryable
var q = from c in dbContext.Customers Where c.City == ":London" select c;
var finalAnswer = from c in q order by c.Name select c;
// 2. use Enumerable
var q = (from c in dbContext.Customers where c.City == "London" select c).AsEnumerable();
var finaAnswer = from c in q order by c.Name select c;
方法1,采用的是 IQueryable
方法2,则是把数据库对象从 IQueryable 强制转为了 IEnumerable形式的标准可枚举对象,即先向数据库发送查询请求,获得所有的数据后,放在本地进行排序操作。
假如每种方法第二行还有一次 where 筛选一部分数据,那么方法1会组合2次 where,数据库只会返回最终目标数据集。而方法2会先从数据把所有第一次 where 得到的数据传到本地,之后在本地再次筛选,无疑增加了网络数据传输量。
4.2 Provider
IQueryable 的 Provider 未必能支持每一种查询方式,只能支持某些固定的运算符、方法,所以一旦查询操作里面调用除此之外的方法,那么就有可能把序列当成 IEnumerable 来查询,而非 IQueryable,否则会抛出异常。
再上 《Effective c#》例子:
private bool isValidProduct(Product p) => p.ProductName.LastIndexOf('C') == 0;
// 1. 转为 Enumerable 执行
var q1 = from p in dbContext.Products.AsEnumerable() where isValidProduct(p) select p;
// 2. 直接执行 IsValidProduct
var q2 = from p in dbContext.Products where isValidProduct(p) select p;
方法1可以正常运行,只是在 AsEnumerable() 之后,查询必须在本地执行,where 字句内的逻辑由LINQ to Objects处理。
方法2则会抛出异常,因为IQueryProvider会把查询操作转为T-SQL,交由远端执行。