LINQ 概述

本文介绍了C#中的LINQ(Language Integrated Query)技术,它提供了统一的查询语法来访问不同数据源。通过示例展示了如何使用LINQ查询语言进行数据操作,包括查询表达式的结构、扩展方法的工作原理以及延迟执行的概念。此外,文章还探讨了如何在C#中利用LINQ对列表和实体进行筛选、排序和转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LINQ(Language Integrated Query,语言集成查询)在C#编程语言中集成了查询语法,可以用相同的语法访问不同的数据源。LINQ提供了不同数据源的抽象层,所以可以使用相同的语法。

本章介绍LINQ的核心原理和C#中支持C# LINQ Query的语言扩展。

在介绍LINQ的特性之前,本节先介绍一个简单的LINQ查询。C#提供了转换为方法调用的集成查询语言。下面说明这个转换的过程,以便用户使用LINQ的全部功能。

列表和实体

示例的LINQ查询在一个包含1950-2016年一级方程式锦标赛的集合上进行。这些数据需要使用.NET标准库中的类和列表来准备。

这个库使用了如下名称空间:

System

System.Collections.Generic

对于实体,定义类型Racer。Racer定义了几个属性和一个重载的ToString()方法,该方法以字符串格式显示赛车手。这个类实现了IFormattable接口,以支持格式字符串的不同变体,这个类还实现了IComparable<Racer>接口,它根据LastName为一组赛车手排序。为了执行更高级的查询,Racer类不仅包含单值属性,如FirstName、LastName、Wins、Country和Starts,还包含多值属性,如Cars和Years。Years属性列出了赛车手获得冠军的年份。一些赛车手曾多次获得冠军。Cars属性用于列出赛车手在获得冠军的年份中使用的所有车型。

    public class Racer : IComparable<Racer>, IFormattable {
        public Racer(string firstName,string lastName,string country,int starts,int wins)
         : this(firstName,lastName,country,starts,wins,null,null){}
        public Racer (string firstName, string lastName, string country, int starts,
            int wins, IEnumerable<int> years, IEnumerable<string> cars) {
            FirstName = firstName;
            LastName = lastName;
            Country = country;
            Starts = starts;
            Wins = wins;
            Years = years != null ? new List<int> (years) : new List<int> ();
            Cars = cars != null ? new List<string> (cars) : new List<string> ();

        }
        public string FirstName { get; }
        public string LastName { get; }
        public int Wins { get; }
        public string Country { get; }
        public int Starts { get; }
        public IEnumerable<string> Cars { get; }
        public IEnumerable<int> Years { get; }
        public override string ToString()=>$"{FirstName} {LastName}";
        public int CompareTo(Racer other)=>LastName.CompareTo(other?.LastName);
        public string ToString(string format)=>ToString(format,null);
        public string ToString(string format,IFormatProvider formatProvider)
        {
            switch(format)
            {
                case null:
                return null;
                case "N":
                return ToString();
                case "F":
                return FirstName;
                case "L":
                return LastName;
                case "C":
                return Country;
                case "S":
                return Starts.ToString();
                case "W":
                return Wins.ToString();
                case "A":
                return $"{FirstName},{LastName},{Country}; starts:{Starts}, wins:{Wins}";
                default:
                throw new FormatException($"Format {format} not supported");
            }

        }

第二个实体类是Team。这个类仅包含车队冠军的名字和获得冠军的年份。与赛车手冠军类似,针对一年中最好的车队也有一个冠军奖项:

        public class Team
        {
            public Team(string name,params int[] years)
            {
                Name = name;
                Years = years != null ?new List<int>(years):new List<int>();
            }
            public string Name{get;}
            public IEnumerable<int> Years{get;}
        }

Formula1类在GetChampions()方法中返回一组赛车手。这个列表包含了1950-2016年之间的所有一级方程式冠军。

        public class Formula1
        {
            private static List<Racer> s_racers;
            public static IList<Racer> GetChampions()=>
            s_racers??(s_racers = InitializeRacers());
            private static List<Racer> InitializeRacers()=>
            new List<Racer>()
            {
                new Racer("Nino","Farina","Italy",33,5,new int[]{1950},new string[] {"Alfa Romeo"}),
                new Racer("Alberto","Ascari","Italy",32,10,new int[]{1952,1953},new string[]{"Ferrari"}),
                new Racer("Juan Manuel","Fangio","Argentina",51,24,new int[]{1951,1954,1955,1956,1957},
                new string[]{"Alfa Remeo","Maserati","Mercedes","Ferrari"}),
                new Racer("Mike","Hawthorn","UK",45,3,new int[]{1958},new string[]{"Ferrari"}),
                new Racer("Phil","Hill","USA",48,3,new int[]{1961},new string[]{"Ferrari"}),
                new Racer("John","Surtees","UK",111,6,new int[]{1964},new string[]{"Ferrari"}),
                new Racer("Jim","Clark","UK",72,25,new int[]{1963,1965},new string[]{"Lotus"}),
                new Racer("Jack","Brabham","Australia",125,14,new int[]{1959,1960,1966},new string[]{"Cooper","Brabham"}),
                new Racer("Denny","Hulme","New Zealand",112,8,new int[]{1967},new string[]{"Brabham"}),
                new Racer("Graham","Hill","UK",176,14,new int[]{1962,1968},new string[]{"BRM","Lotus"}),
                new Racer("Jochen","Rindt","Austria",60,6,new int[]{1970},new string[]{"Lotus"}),
                new Racer("Jackie","Stewart","UK",99,27,new int[]{1969,1971,1973},new string[]{"Metra","Tyrrell"})
            };
        }

对于后面在多个列表中执行的查询,GetConstructorChampions()方法返回所有的车队冠军列表。车队冠军是从1958年开始设立的。

            private static List<Team> s_teams;
            public static IList<Team> GetConstructorChampions()
            {
                if(s_teams == null)
                {
                    s_teams = new List<Team>()
                    {
                        new Team("Vanwall",1958),
                        new Team("Cooper",1959,1960),
                        new Team("Ferrari",1961,1964,1975,1976,1977,1979,1982,1983,1999,2000,
                        2001,2002,2003,2004,2007,2008),
                        new Team("BRM",1962),
                        new Team("Lotus",1963,1965,1968,1970,1972,1973,1978),
                        new Team("Brabham",1966,1967),
                        new Team("Matra",1969),
                        new Team("Tyrrell",1971),
                        new Team("Mclaren",1974,1984,1985,1988,1989,1990,1991,1998),
                        new Team("Williams",1980,1981,1986,1987,1992,1993,1994,1996,1997),
                        new Team("Benetton",1995),
                        new Team("Renault",2005,2006),
                        new Team("Brawn GP",2009),
                        new Team("Red Bull Racing",2010,2011,2012,2013),
                        new Team("Mercedes",2014,2015,2016,2017)
                    };
                }
                return s_teams;
            }

LINQ查询

演示LINQ的示例应用程序是一个控制台应用程序,使用了如下名称空间:

System

System.Collections.Generic

System.Linq

在以前创建的库中,使用这些准备好的列表和实体,进行LINQ查询,例如,查询来自意大利的所有世界冠军,并按照夺冠次数排序。为此可以使用List<T>类的方法,如FindAll()和Sort()方法。而使用LINQ的语法非常简单:

            public static void LINQQuery () {
                var query = from r in Formula1.GetChampions ()
                where r.Country == "Italy"
                orderby r.Wins descending
                select r;
                foreach (Racer r in query) {
                    System.Console.WriteLine($"{r:A}");

                }
            }

这个查询的结果显示了来自意大利的所有世界冠军,并排好序:

Alberto,Ascari,Italy; starts:32, wins:10
Nino,Farina,Italy; starts:33, wins:5

表达式

                var query = from r in Formula1.GetChampions ()
                where r.Country == "Italy"
                orderby r.Wins descending
                select r;

是一个LINQ查询。子句from、where、orderby、descending和select都是这个查询中预定义的关键字。

查询表达式必须以from子句开头,以select或group子句结束。在这两个子句之间,可以使用where、orderby、join、let和其他from子句。

注:变量query只指定了LINQ查询。该查询不是通过这个赋值语句进行的,只要使用foreach循环访问查询,该查询就会执行。

扩展方法

编译器会转换LINQ查询,以调用方法而不是LINQ查询。LINQ为IEnumerable<T>接口提供了各种扩展方法,以便用户在实现了该接口的任意集合上使用LINQ查询。扩展方法在静态类中声明,定义为一个静态方法,其中第一个参数定义了它扩展的类型。

扩展方法可以将方法写入最初没有提供该方法的类中。还可以把方法添加到实现某个特定接口的任何类中,这样多个类就可以使用相同的实现代码。

例如,String类没有Foo()方法。String类是密封的,所以不能从这个类中继承。但可以创建一个扩展方法,如下所示:

    public static class StringExtension {
        public static void Foo (this string s) {
            System.Console.WriteLine ($"Foo invoked for {s}");

        }
    }

Foo()方法扩展了String类,因为它的第一个参数定义为String类型。为了区分扩展方法和一般的静态方法,扩展方法还需要对第一个参数使用this关键字,现在就可以使用带string类型的Foo()方法了:

            string s = "Hello";
            s.Foo();

结果在控制台上显示"Foo Invoked for Hello",因为Hello是传递给Foo()方法的字符串。

也许这看起来违反了面向对象的规则,因为给一个类型定义了新方法,但没有改变该类型或派生自它的类型。但实际上并非如此。扩展方法不能访问它扩展的类型的私有成员。调用扩展方法只是调用静态方法的一种新语法。对于字符串,可以用如下方式调用Foo()方法,获得相同的结果:

            string s = "Hello";
            //s.Foo();
            StringExtension.Foo(s);

要调用静态方法,应在类名的后面加上方法名。扩展方法是调用静态方法的另一种方式。不必提供定义了静态方法的类名,相反,编译器调用静态方法是因为它带的参数类型。只需要导入包含该类的名称空间,就可以将Foo()扩展方法放在String类的作用域中。

定义LINQ扩展方法的一个类是System.Linq名称空间中的Enumerable。只需要导入这个名称空间,就可以打开这个类的扩展方法的作用域。下面列出了Where()扩展方法的实现 代码。Where()扩展方法的第一个参数包含了this关键字,其类型是IEnumerable<T>。这样,Where()方法就可以用于实现IEnumerable<T>的每个类型。例如,数组和List<T>类实现了IEnumerable<T>接口。第二个参数是一个Func<T,bool>委托,它引用了一个返回布尔值、参数类型为T的方法。这个谓词在实现代码中调用,检查IEnumerable<T>源中的项是否应放在目标集合中。如果委托引用了该方法,yield return语句就将源中的项返回给目标。

        public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,
        Func<TSource,bool> predicate)
        {
            foreach(TSource item in source)
            {
                if(predicate(item))
                yield return item;
            }
        }

因为Where()作为一个泛型方法实现,所以它可以用于包含在集合中的任意类型。实现了IEnumerable<T>接口的任意集合都支持它。

注:这里的扩展方法在System.Core程序集的System.Linq名称空间中定义。

现在就可以使用Enumerable类中的扩展方法Where()、OrderByDescending()和Select()。这些方法都返回IEnumerable<TSource>,所以可以使用前面的结果依次调用这些方法。通过扩展方法的参数,使用定义了委托参数的实现代码的匿名方法。

        static void ExtensionMethods()
        {
            var champions = new List<Racer>(Formula1.GetChampions());
            IEnumerable<Racer> italyChampions = champions.Where(r=>r.Country == "Italy")
                               .OrderByDescending(r=>r.Wins)
                               .Select(r=>r);
            foreach(Racer r in italyChampions)
            {
                System.Console.WriteLine($"{r:A}");

            }
        }

推迟查询的执行

在运行期间定义查询表达式时,查询就不会运行。查询会在迭代数据项时运行。

再看看扩展方法Where()。它使用yield rerurn语句返回谓词为true的元素。因为使用了yield return语句,所以编译器会创建一个枚举器,在访问枚举中的项后,就返回它们。

        public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,
        Func<TSource,bool> predicate)
        {
            foreach(TSource item in source)
            {
                if(predicate(item))
                yield return item;
            }
        }

这是一个非常有趣也非常重要的结果。在下面的例子中,创建了String元素的一个集合,用名称填充它。接着定义一个查询,从集合中找出以字母J开头的所有名称。集合也应是排好序的。在定义查询时,不会进行迭代。相反,迭代在foreach语句中进行,在其中迭代所有的项。集合中只有一个元素Juan满足where表达式的要求,即以字母J开头。迭代完成后,将Juan写入控制台。之后在集合中添加4个新名称,再次进行迭代。

        static void DeferredQuery()
        {
            var names = new List<string> {"Nino","Alberto","Juan","Mike","Phil"};
            var namesWithJ = from n in names
            where n.StartsWith("J")
            orderby n
            select n;
            System.Console.WriteLine("First iteration");
            foreach(string name in namesWithJ)
            {
                System.Console.WriteLine(name);
            }
            names.Add("John");
            names.Add("Jim");
            names.Add("Jack");
            names.Add("Denny");
            System.Console.WriteLine("Second iteration");
            foreach(string name in namesWithJ)
            {
                System.Console.WriteLine(name);
            }
        }

 因为迭代在查询定义时不会进行,而是在执行每个foreach语句时进行,所以可以看到其中的变化,如应用程序的结果所示:

First iteration
Juan
Second iteration
Jack
Jim
John
Juan

当然,还必须注意,每次在迭代使用查询时,都会 调用扩展方法。大多数情况下,这是非常有效的,因为我们可以检测出源数据中的变化。但在一些情况下,这是不可行的。调用扩展方法ToArray()、ToList()等可以改变这个操作。在示例中,ToList遍历集合,返回一个实现了IList<string>的集合。之后对返回的列表遍历两次,在两次迭代之间,数据源得到了新名称。

        static void DeferredQuery()
        {
            var names = new List<string> {"Nino","Alberto","Juan","Mike","Phil"};
            var namesWithJ = (from n in names
            where n.StartsWith("J")
            orderby n
            select n).ToList();
            System.Console.WriteLine("First iteration");
            foreach(string name in namesWithJ)
            {
                System.Console.WriteLine(name);
            }
            names.Add("John");
            names.Add("Jim");
            names.Add("Jack");
            names.Add("Denny");
            System.Console.WriteLine("Second iteration");
            foreach(string name in namesWithJ)
            {
                System.Console.WriteLine(name);
            }
        }

在结果中可以看到,在两次迭代之间输出保持不变,但集合中的值改变了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值