对于复杂的对象列表,运行时引擎如何才能通过比较确定两个对象是否重复?对于复杂对象,必须提供一个比较器,即实现IEqualityComparer(Of T)执行比较的一个类实例。
假设有一个包括客户信息的序列,你希望得到这些客户坐在国家的专门列表。如果已有一个简单的国家列表,可使用默认比较器来比较字符串。但有可能面临的是一个客户类表(你当然可以使用select方法投影列表以仅拥有国家列表,但这样你会丢失所有其余数据,造成使用列表的其他计算失效)。
先看下IEqualityComparer的实现
class="code">Public Class CustomerCountryComparer Implements IEqualityComparer(Of Customer) Public Function Equals1(x As Customer, y As Customer) As Boolean Implements IEqualityComparer(Of Customer).Equals Return x.Country.Equals(y.Country) End Function Public Function GetHashCode1(obj As Customer) As Integer Implements IEqualityComparer(Of Customer).GetHashCode Return obj.Country.GetHashCode() End Function End Class
再看我们两个不同的实现,两个输出的结果并不一样,第一个会按照Country、ContactName进行distinct
Dim db As New SimpleDataContext Dim customersOFcountries = db.Customers _ .Select(Function(customer) New With {customer.Country, customer.ContactName}) _ .Distinct() Console.WriteLine("Country Info:") For Each country In customersOFcountries Console.WriteLine(" " & country.ContactName & " live in " & country.Country) Next Console.WriteLine() Dim customersOFcountries1 = db.Customers.Distinct(New CustomerCountryComparer) _ .Select(Function(customer) New With {customer.Country, customer.CustomerId, customer.ContactName}) For Each country In customersOFcountries1 Console.WriteLine(country.CustomerId & "." & country.ContactName & " live in " & country.Country) Next
在第一个实现中,distinct会以country、contact为key,即使用默认的比较器,进行比较;而在第二个实现中,则
采用指定的比较器进行比较,可以看到会先按照比较器,组成一个county唯一的列表,再将其中的信息进行输出,而不是按照输出信息进行distinct操作。
在实际工作中,我们很可能需要妥善的处理空序列,此时我们需要Enumerable.DefaultIfEmpty方法,当我们选择序列为空时,则返回默认值的实例。
Dim noCustomers = db.Customers.Where( _ Function(customer) customer.Country = "XXX") Dim noCustomers1 = noCustomers.DefaultIfEmpty( _ New Customer With {.CustomerId = "XXXXXXXXX"}) Dim results = String.Format( _ "noCustomers contains {0} element(s) . " & _ "noCustomers1 contains {1} elemetn(s). ", _ noCustomers.Count(), noCustomers1.Count()) Console.WriteLine(results) For Each customer As Customer In noCustomers1 Console.WriteLine(customer.CustomerId) Next
在上面代码中,使用了空列表来填充noCustomers变量(没有国家为XXX的客户),然后使用DefaultIfEmpty方法创建了noCustomers1,并为其提供了默认值。
可以使用Enumerable.OrderBy,Enumerable.ThenBy,Enumerable.OrderByDescending和Enumerable.ThenByDescending先提供初始排序,然后提供一个或多个次要排序(使用升序或降序排序)。
Dim customers = db.Customers.Where( _ Function(customer) customer.ContactTitle = "Owner") Dim results = customers _ .OrderBy(Function(customer) customer.Country) _ .ThenByDescending(Function(customer) customer.City) _ .ThenBy(Function(customer) customer.ContactName) _ .Select(Function(customer) String.Format("({0},{1}) {2}", _ customer.Country, customer.City, customer.ContactName)) For Each result As String In results Console.WriteLine(result) Next
按照上述代码,输出的结果为,将contactTitle为owner的客户,首先按照国家排序,然后按照城市降序排序,最后按照每个城市中的联系人姓名进行排序。
一些应该程序要求您确认序列是否满足某些特定的条件。是否有任何元素满足某个条件吗?是所有元素都满足个条件吗?序列中是否出现了某个特定的项目?两个序列是否相等?Enumerable类规定了用于提供所有此类信息的方法。
要确定序列是否包含某个元素,请调用不包含任何参数的Enumerable.Any方法。
要确认序列中是否包含满足某个条件的的元素,请使用此方法,将这个特定条件通过lambda表达式传递给Enumerable.Any方法。如果输入序列中存在元素或者存在匹配提供条件的元素,则Enumerable.Any方法放回true
Dim results = db.Products _ .Where(Function(product) product.CategoryId = 1) 'determine if a list has any elements: Dim anyElements = results.Any() 'determine list match extension method Dim matchingElements = results _ .Any(Function(product) product.ProductName.StartsWith("M")) Console.WriteLine("list has any elements ? {0}", anyElements) Console.WriteLine("list has elements matching the method ? {0}", _ matchingElements)
要确定序列的所有成员是否均满足某个条件,请提供一个指定条件的方法,并调用Enumerable.All方法。
Dim results = db.Products _ .Where(Function(product) product.CategoryId = 1) Dim allElements = results _ .All(Function(product) product.UnitsInStock > 5) Console.WriteLine("all elements match the method? {0}", allElements)
要确定序列是否包含某个特定元素,请调用Enumerable.contains.如果要搜索一个简单的值(如字符串或整数),可使用默认比较器,并且仅需提供搜索的值即可。如果尝试确定序列中是否存在更为复杂的对象,必须提供IEqualityComparer(Of T)的实例。
Dim numbers = Enumerable.Range(1, 10) Dim contains5 = numbers.Contains(5) Dim db As New SimpleDataContext Dim results = db.Customers _ .Where(Function(customer) customer.ContactTitle = "Owner") Dim item As New Customer _ With {.ContactName = "Bob", .Country = "USA"} Dim containsUsa = results.Contains(item, New CustomerCountryComparer) Console.WriteLine("numbers has 5? {0}", contains5) Console.WriteLine("containsUsa ? {0}", containsUsa)
要确定两个序列是否相等,请调用Enumerable.SequenceEqual方法,与Enumerable.Contains方法一样,可使用默认比较器比较包含简单值的两个序列,也可以使用自定义比较器比较包含更复杂对象的两个序列。
如果两个序列并不包含相同的类型或长度不同,比较立即失败。如果它们包含相同的类型且长度相同,Enumerable.SquenceEqual方法将使用指定的比较器比较每个item。
Const count As Integer = 10 Dim rnd As New Random Dim start = rnd.Next Dim s1 = Enumerable.Range(start, count) start = rnd.Next Dim s2 = Enumerable.Range(start, count) Dim sequencesEqual = s1.SequenceEqual(s2) Console.WriteLine("SequencesEqual = {0}", sequencesEqual) Dim db As New SimpleDataContext Dim customers1 = db.Customers _ .Where(Function(customer) customer.ContactTitle = "Owner") Dim customers2 = db.Customers _ .Where(Function(customer) customer.ContactTitle = "Accounting Manager") sequencesEqual = _ customers1.SequenceEqual(customers2, New CustomerCountryComparer) Console.WriteLine("SequencesEqual = {0}", sequencesEqual)
如果你需要获取可枚举序列的处理结果,并将其传递给某个要求特定类型的方法,或者您需要使用存储在可枚举序列中的数据来调用某一特定类型的方法,可能需要调用其参数需要一个Enumerable类型变量方法,以将该数据转换为其他类型。
我们拿String.Join为例,可能不太合适,该方法在.net 3.5中的签名为Join(String,string())。
假设我们需要结果为筛选后结果的字符串,并用“,”间隔。
Dim db As New SimpleDataContext Dim customers = db.Customers _ .Where(Function(customer) customer.Country = "France") _ .Select(Function(customer) customer.ContactName) Dim nameList = String.Join(",", customers.ToArray())
实际上在.net 4.5中,String.Join()已经有了Join(string,IEnumerable(Of string))的重载。
尽管可以从可枚举序列转换为通用的Dictonary,但必须至少提供一个函数来指示生成键值的方式。Enumerable.ToDictionary方式提供了多个重载,允许你指定键选择器、值选择器、键比较器以及值比较器方法的各种组合。
Dim db As New SimpleDataContext Dim someProducts = db.Products _ .Where(Function(product) product.CategoryId = 1) Dim productsDictionary = _ someProducts.ToDictionary(Function(product) product.ProductId) 'display the contents of the dictionary : For Each keyValuePair As KeyValuePair(Of Integer,Product) In productsDictionary Console.WriteLine("{0},{1}", _ keyValuePair.Key, keyValuePair.Value.ProductName) Next
上述代码,使用productID字段作为键值来获取someProducts变量的内容,并将其转换为Dictionary.然后将代码将遍历字典中的所有项目并打印键和每个字典项目的值对应的一个字段。
如果你希望使用一个并非简单类型的值(如字典中的键),该怎么办?或许你希望使用整个Product作为字典键。如果是这样,必须再次提供一个自定义比较器的实例,以给出一种比较各个键实例的方式。
Public Class ProductComparer Implements IEqualityComparer(Of Product) Public Function Equals1(x As Product, y As Product) As Boolean _ Implements IEqualityComparer(Of Product).Equals Return x.ProductId.Equals(y.ProductId) End Function Public Function GetHashCode1(obj As Product) As Integer _ Implements IEqualityComparer(Of Product).GetHashCode Return obj.GetHashCode() End Function End Class
Dim productsDictionary1 As Dictionary(Of Product, String) = _ someProducts.ToDictionary(Function(pro) pro, _ Function(pro) pro.ProductName, New ProductComparer) For Each keyValuePair As KeyValuePair(Of Product,String) In productsDictionary1 Console.WriteLine("{0},{1}", keyValuePair.Key.ProductId, keyValuePair.Value) Next
上述代码,使用整个Product作为字典中每个项目的键值;只需要提供一个自定义类来实现 IEqualityComparer(Of Product),此类将以productID为标准,来判断Product是否相等。
一些方法明确要求以通用列表List作为输入,而不是可枚举序列。要准换为List,请使用Enumerable.ToList方法。
Dim productNames = db.Products _ .Select(Function(product) product.ProductName) _ .ToList() Dim results = String.Format("Chang was found at index {0}", _ productNames.IndexOf("Chang"))
Dictionary数据结构将键映射为单个值。Lookup数据结构将键映射为一组值。此结构是分层Enumerable实例的最佳配项(例如,链接到许多Product实例的CategoryID)。Enumerable.ToLookup方法会为你执行转换(假设你有一个简单的一键对多值的层次结构)。
Dim db As New SimpleDataContext Dim products = db.Products _ .Where(Function(product) product.UnitPrice > 40) _ .OrderBy(Function(product) product.CategoryId) Dim lookup = products _ .ToLookup(Function(product) product.CategoryId, _ Function(product) product) Dim sw = New StringWriter For Each grouping As IGrouping(Of Integer?,Product) In lookup sw.WriteLine("Category ID = {0}", grouping.Key) For Each product As Product In grouping sw.WriteLine(" {0} ({1:C})", _ product.ProductName, product.UnitPrice) Next Next Console.WriteLine(sw.ToString())
上述代码,将IEnumerable(Of Product)序列转换为Lookup,其中每个键都是CategoryID,对应的值可以理解为
Product的集合,此时categoryID和product是一对多的关系。ToLookup方法的第一个参数是用来确定键的函数,第二个函数用来确定各项目值的函数。
在使用LINQ查询非泛型IEnumerable集合(ArrayList)时,你必须显式声明范围变量的类型以反映此集合中对象的特定类型。
Dim que = From item As String In items
通过指定范围变量的类型,你将ArrayList中的各项强制转换为string。
在查询表达式中,调用Cast(OF TResult)方法与使用显式类型化的范围变量等效。注意如果无法执行指定的强制转换,则Cast(of TResult)引发异常。出现这种情况,你可以在转换之前使用OfType方法对列表进行筛选。 Cast(of TResult)和OfType(Of TResult)是两种对非泛型IEnumerable类型操作的标准查询运算符方法。
Dim items As New ArrayList items.Add("January") items.Add(1) items.Add("August") items.Add(14) items.Add("October") items.Add("April") items.Add(38) Dim stringItem = items.OfType(Of String)() Dim query = stringItem.Cast(Of String)() Dim results = query.Where(Function(item) item.StartsWith("A")) For Each result As String In results Console.WriteLine(result) Next
上述代码,将筛选ArrayList数据,以仅检索以字母“A”开头的项目。
最后,Enumerable.AsEnumerable方法允许你将源类型视为IEnumerable,这样你便可以使用IEnumerable的方法,而不是已实现类中的方法。这些方法在一些特定情况下非常有用,但是在常规编码中不大可能用到它。
假设在LINQ使用延迟执行来检索数据,而你希望虚拟化对大型数据集的访问,这时你需要采用某种方式从数据源检索特定的行号(从数据内的特定偏移开始)。要满足这些需求,可以使用Enumerable.Take和Enumerable.Skip方法。这些方法允许你在开始获取行之前就指定要获取的行号和要跳过的行号。
Dim db As New SimpleDataContext Dim products = db.Products _ .OrderBy(Function(product) product.ProductName) _ .Select(Function(product) String.Format("{0}:{1}", _ product.ProductId, product.ProductName)) _ .Skip(10).Take(5)
上述代码,将跳过10行后返回5行;
你可以使用Enumerable.TakeWhile和Enumerable.SkpWhile方法,通过设置条件获取和跳过序列中的值。当条件为真时,TakeWhile方法会获取值并返回一个序列,其中将包括获取的所有值。只要条件为真,SkipWhile方法会跳过值并返回输入序列的其余部分。