我将给出不同的例子,因为我不太关注你的例子,并用它来解释加入的基础知识,希望能达到你需要学习的内容。
让我们想象两个比 LocationThing 等名称稍多一些的类(这让我迷失了)。
public class Language
{
string Code{get;set;}
string EnglishName{get;set;}
string NativeName{get;set;}
}
public class Document
{
public int ID{get; private set;}//no public set as it corresponds to an automatically-set column
public string LanguageCode{get;set;}
public string Title{get;set;}
public string Text{get;set;}
}
现在,让我们也假设我们有方法GetLanguages()
并GetDocuments()
分别返回所有语言和文档。有几种不同的方法可以工作,我稍后会谈到。
一个有用的连接示例是,如果我们想要所有标题和它们所在语言的所有英文名称。在 SQL 中,我们将使用:
SELECT documents.title, languages.englishName
FROM languages JOIN documents
ON languages.code = documents.languageCode
或者省略表名,这样做不会使列名模棱两可:
SELECT title, englishName
FROM languages JOIN documents
ON code = languageCode
对于文档中的每一行,它们中的每一个都会将它们与语言中的相应行进行匹配,并返回组合行的标题和英文名称(如果存在没有匹配语言的文档,则不会返回,如果有两种语言具有相同的代码 - 但在这种情况下,db 应该阻止 - 每种语言都会提到一次相应的文档)。
LINQ 等价物是:
from l in GetLanguages()
join d in GetDocuments()
on l.Code equals d.LanguageCode //note l must come before d
select new{d.Title, l.EnglishName}
这将类似地将每个文档与其相应的语言匹配并返回一个IQueryable<T>
或IEnumerable<T>
(取决于源枚举/可查询),其中T
是具有Title
和EnglishName
属性的匿名对象。
现在,至于这方面的费用。这主要取决于GetLanguages()
和的性质GetDocuments()
。
不管来源是什么,这本质上是搜索这两种方法的每一个结果的问题——这就是操作的本质。但是,最有效的方法仍然是根据我们对源数据的了解而有所不同。让我们首先考虑一个 Linq2Objects 表单。有很多方法可以做到这一点,但让我们想象他们正在返回List
预先计算好的 s:
public List<Document> GetDocuments()
{
return _precomputedDocs;
}
public List<Language> GetLanguages()
{
return _precomputedLangs;
}
让我们暂时假设 Linqjoin
不存在,并想象我们将如何编写功能上与上面的代码等效的东西。我们可能会得出这样的结论:
var langLookup = GetLanguages().ToLookup(l => l.Code);
foreach(var doc in GetDocuments())
foreach(var lang in langLookup[doc.LanguageCode])
yield return new{doc.Title, lang.EnglishName};
这是一个合理的一般情况。我们可以更进一步,减少存储空间,因为我们知道每种语言最终关心的只是英文名称:
var langLookup = GetLanguages().ToLookup(l => l.Code, l => l.EnglishName);
foreach(var doc in GetDocuments())
foreach(var englishName in langLookup[doc.LanguageCode])
yield return new{doc.Title, EnglishName = englishName};
这大约是我们在没有数据集的特殊知识的情况下所能做的。
如果我们确实有特殊的知识,我们可以走得更远。例如,如果我们知道每个代码只有一种语言,那么下面的代码会更快:
var langLookup = GetLanguages().ToDictionary(l => l.Code, l => l.EnglishName);
string englishName;
foreach(var doc in GetDocuments())
if(langLookup.TryGetValue(doc.LanguageCode, out englishName))
yield return new{doc.Title, EnglishName = englishName};
如果我们知道这两个来源都是按语言代码排序的,我们可以更进一步,同时遍历它们,产生匹配,并在处理完它们后丢弃语言,因为我们永远不会这样做枚举的其余部分再次需要它。
但是,仅查看两个列表时,Linq 并没有这种特殊知识。它知道每一种语言和每一个文件都有相同的代码。它真的必须检查很多才能找出答案。为此,它的工作方式非常有效(由于一些优化,比我上面的示例建议的要好一点)。
让我们考虑一个 Linq2SQL 案例,并注意实体框架和其他直接在数据库上使用 Linq 的方法是可比较的。假设所有这些都发生在一个类的上下文中,该类的_ctx
成员是DataContext
. 那么我们的源方法可以是:
public Table<Document> GetDocuments()
{
return _ctx.GetTable<Document>();
}
public Table<Language> GetLanguages()
{
return _ctx.GetTable<Languages>();
}
Table<T>
IQueryable<T>
与其他一些方法一起实现。在这里,它不会加入内存中的内容,而是执行以下(除去一些别名)SQL:
SELECT documents.title, languages.englishName
FROM languages JOIN documents
ON languages.code = documents.languageCode
看起来熟悉?就是我们一开始提到的那个SQL。
关于这一点的第一件好事是,它不会从数据库中带回我们不会使用的任何东西。
第二件好事是数据库的查询引擎(将其转换为可执行代码然后运行)确实了解数据的性质。例如,如果我们将表设置为在Languages
列上具有唯一键或约束code
,引擎知道不可能有两种语言具有相同的代码,因此它可以执行我们上面提到的优化的等效项使用 aDictionary
而不是 a ILookup
。
第三件好事是,如果我们有索引languages.code
,documents.languageCode
然后查询引擎将使用这些索引进行更快的检索和匹配,也许可以从索引中获取所需的所有内容而无需访问表,调用先访问哪个表避免在第二个中测试不相关的行,依此类推。
第四件好事是,RDBMS 已经从几十年来关于如何尽可能快地进行这种检索的研究中受益,所以我们有一些我不知道也不需要知道的事情从。。得到好处。
总之,我们希望直接针对数据源运行查询,而不是针对内存中的源。也有例外,特别是某些形式的分组(通过一些分组操作直接命中数据库可能意味着重复命中它),如果我们快速连续地一遍又一遍地重复使用相同的结果(在这种情况下,我们最好点击它一次用于这些结果,然后存储它们)。