C# LINQ用法

160 阅读10分钟
  • LINQ中提供了很多集合的扩展方法,配合lambda表达式能够简化数据处理
  • 不能直接使用,需要引入命名空间。using System.Linq(非泛型)、或using System.Collections.Generic(泛型)

有关类

Enumerable静态类

namespace System.Linq

静态方法

方法名说明
Repeat
Range
Repeat("abc", 3)//得到一个字符串序列

Range(1, 3)    //生成1 2 3序列

IEnumerable

官方声明

我们先去看看公开的.Net4.0的源程序中IEnumerable、IEnumerable、IEnumerator和IEnumerator这四个接口是如何声明的

public interface IEnumerable<out T> : IEnumerable {
    new IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T> : IDisposable, IEnumerator {
    new T Current {
        get;
    }
}

public interface IEnumerable {
    IEnumerator GetEnumerator();
}

public interface IEnumerator {
    bool MoveNext();
    Object Current {
        get;
    }
    void Reset();
}

接口IEnumerable实现

1、建一个学生数据结构和一个学生集合类:

    //student数据结构
    class Student
    {
        public int id;
        public string name;
    }

    //student 集合
    class StudentCollection
    {
        public List<Student> students = new List<Student>();
        //公开一个Add()方法以添加数据
        public void Add(Student student)
        {
            students.Add(student);
        }
    }

接下来添加数据

        static void Main(string[] args)
        {
            StudentCollection sc = new StudentCollection();
            sc.Add(new Student { id=0,name="Tony"});
            sc.Add(new Student { id=1,name="Micheal"});
            sc.Add(new Student { id =2, name = "Amy" });
            foreach(var s in sc) {...}
        }

当我们想用foreach()遍历的时候,编译器会告诉我们StudentCollection不包含GetEnumerator,不能用foreach遍历。虽然StudentCollection里面有能用遍历的List,但我们不想在属性上迭代,我们想在类上迭代,不能 foreach(var s in sc.students){...}

现在只有把我们的StudentCollection类改造成能foreach的

2、继承接口IEnumerable

构造原生GetEnumrator方法

这种是利用枚举器的工作原理来写的

当我们在类后面加上:IEnumerable后,会加上个返回迭代器的方法GetEnumrator

class StudentCollection:IEnumerable
{
    public List<Student> students = new List<Student>();
    public void Add(Student student)
    {
        students.Add(student);
    }

    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

下面按照IEnumetator接口的约定来实现我们的迭代器

迭代器工作的原理是:先调用MoveNext()方法,然后读取Current得到元素,直到MoveNext返回false

Reset应该是回到第一个元素。但根据MSDN上面的说法,Reset 方法提供的 COM 互操作性。它不一定需要实现;相反,实施者只需抛出NotSupportedException。但是,如果您选择执行此操作,则应确保没有调用方依赖于Reset功能

//迭代器
class StudentCollectionEnumerator : IEnumerator
{

    //Current返回当前元素

    public object Current
    {
        get
        {
            throw new NotImplementedException();
        }
    }

    //MoveNext移动到下一个

    public bool MoveNext()
    {
        throw new NotImplementedException();
    }

    public void Reset()
    {
        throw new NotImplementedException();
    }
}

我们需要3个字段分别放置 元素的位置、元素、元素集。改变后的程序如下

//迭代器
class StudentCollectionEnumerator : IEnumerator
{
    private int _index;
    private List<Student> _collection;
    private Student value;
    public StudentCollectionEnumerator(List<Student> colletion)
    {
        //首先,迭代器初始化,引入元素集 _collection
        _collection = colletion;
        //把索引 _index设置成-1。
        //设置成-1而不是0是因为迭代器首先调用MoveNext,在MoveNext里面我们先把索引+1指向下一个元素,如果索引_index的值初始为0,则第一个元素是元素集[1],第二个元素了
        _index = -1;
    }
    //第二,我们要把object Current改成 IEnumerator.Current,这个是实现迭代器的关键。返回元素
    object IEnumerator.Current
    {
        get { return value; }
    }
    public bool MoveNext()
    {
        //第三,在MoveNext方法内累加索引,并从元素集中读取元素。
        _index++;
        //让索引值超出元素集返回个false值
        if (_index >= _collection.Count) { return false; }
        else { value = _collection[_index]; }
        return true;
    }
    public void Reset()
    {
        //第四,在Reset方法内让索引值为-1,不过好像直接抛出错误也成
        _index = -1;
    }

}

迭代器写好了,我们在StudentColletion类里面调用

class StudentCollection : IEnumerable
{
    public List students;
    public StudentCollection()
    {
        students = new List();
    }
    public void Add(Student student)
    {
        students.Add(student);
    }
    public IEnumerator GetEnumerator()
    {
        return new StudentCollectionEnumerator(students);
    }
}

测试运行一下,大功告成!我们实现了可枚举的自己的类

利用List构造GetEnumerator

通过观察,发现迭代器主要就是返回一个元素对象,而StudentColletion里面的students元素集是List的,本身就能枚举,我们可以利用这个不用专门写迭代器来实现枚举

class StudentCollection:IEnumerable
{
    public List<Student> students = new List<Student>();
    public void Add(Student student)
    {
        students.Add(student);
    }

    public IEnumerator GetEnumerator()
    {
        foreach(var s in students)
        {
            yield return s;
        }
    }
}

这样就能实现枚举了,真简单,充分利用了.Net给出的各种可枚举集合,不用再去写GetEnumerator这种累活了

实现一个通用可枚举的类

class MyCollection:IEnumerable
{
    public List mycollection = new List();
    public void Add(T value)
    {
        mycollection.Add(value);
    }
    public IEnumerator GetEnumerator()
    {
        foreach(var s in mycollection)
        {
            yield return s;
        }
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        foreach (var s in mycollection)
        {
            yield return s;
        }
    }
}

测试运行

static void Main(string[] args)
{
    MyCollection mc = new MyCollection();
    mc.Add(0);
    mc.Add(1);
    mc.Add(2);
    foreach(var s in mc) { Console.WriteLine(s); }
    Console.ReadKey();
}

如果想用泛型,通用可以仿写

class MyCollection<T>
{
    public List<T> mycollection = new List<T>();
    public void Add(T value)
    {
        mycollection.Add(value);
    }
}

查询语法和方法语法

查询语法更像SQL语句,两种写法编译器的编译结果一样。方法语法更实用

方法语法

var items = list.Where(e => e.Salary > 3000).OrderBy(e => e.Age).Select(e => new
{
    e.Age,e.Name,XB=e.Gender?"男":"女"
});

查询语法

var items2 = from e in list
    where e.Salary > 3000
    orderby e.Age
    select new { e.Age, e.Name, XB = e.Gender ? "男" : "女" };

详解

取元素子序列

Skip、SkipWhile

Skip:限制结果集。跳过从头开始的指定数量的元素,返回剩余元素的IEnumerable序列

Skip(3)    //跳过前三个,取剩余序列

SkipWhile:从头开始跳过序列中满足指定条件的元素,遇到不满足条件的停止执行,返回剩余元素的序列。(故不会取得所有满足条件的序列)

SkipWhile(i => i == 4)
Take、TakeWhile

Take:限制结果集,返回由序列开头数指定数量的元素的序列

list.Skip(3).Take(2)    //跳过前三条,只取两条
Take(20)    //只取前20条,不够20条则有多少取多少

TakeWhile:从头开始取返回满足条件的元素的序列,遇到不满足条件的停止执行。(故不会取得所有满足条件的序列)

TakeWhile(i => i == 4)
Where
Where(i=>i<7)    //
Where((i, k) => i % 2==0)    //选择值为偶数的元素
Where((i, k) => k% 2==0)    //选择为偶数位置的元素
Where((m,k)=>(k>=i&&k<=j))    //取指定区间 [i,j] 的子序列

模仿Where原理

static IEnumerable<int> MyWhere1(IEnumerable<int> items,Func<int,bool> f)
{
    List<int> result = new List<int>();
    foreach(int item in items)
    {
        if(f(item))  result.Add(item);
    }
    return result;
}

优化:使用yield return

static IEnumerable<int> MyWhere1(IEnumerable<int> items,Func<int,bool> f)
{
    foreach(int item in items)
    {
        if(f(item))  yield return item;
    }
}
Distinct

去重

OfType
OfType<int>()    //筛选元素类型是int的序列

投影操作

Select

方法介绍

//参数1是数据项,参数2是下标
Select((i,k) => L(nums, k))

类型转换

Select(i=>Int32.Parse(i.ToString()))    //将序列中的数字字符串转换为数值
Select(i => (double)i)
Select(i => i .ToString())  

和匿名类型、var配合使用

var items= list.Select(e => new {name=e.Name,age=e.Age,gender=e.Gender?"男":"女"})

示例

Select(p=>p.name)    //只取每个对象的name属性
Select(i => i ==2)    //判断序列中的每个元素是否满足某条件,返回true false true这样的序列

//按年龄分组,求各组的最大工资、各组人数
var items = list.GroupBy(e => e.Age)
.Select(g => new { age = g.Key, MaxS = g.Max(e => e.Salary), 
Numbers = g.Count() });

//替代逆序for循环
foreach(int i in nums.Select((m, k) => k).Reverse())

聚合函数

Sum
Max、Min

也可以做字符串比较

Average

返回值double

Count

元素个数或满足条件的元素个数

items.Count(i=>i>1)

items.Count()
Aggregate

可以设置迭代对象的初始值。若不设置,则默认第一个数据为迭代对象初值,从第二个数据开始迭代。

序列中的元素求和、拼接字符串

Aggregate((i, j) => i + j)    //int求和、string拼接字符串。参数1为迭代的对象,参数2为下一个对象

拼接序列中的元素

//拼接数组中的字符串元素,并指定分隔符(这里是空格)
Aggregate((i,j)=>i+" "+j)    

//反转方式拼接字符串
Aggregate((i,j)=>j+" "+i)

最大值、最小值

//最大值
Aggregate((i,k)=>i>k?i:k)
//最小值
Aggregate((i,k)=>i<k?i:k)

删除或替换字符串中的指定字符

//删除str字符串中的指定字符(在InvalidChar中)
InvalidChar.Aggregate(str, (m, k) => m.Replace(k+"",""))

序列的运算

Union

两个序列的并集(去重后),返回新的序列

Intersect

参数为另一个序列,返回交集序列

Except

参数为序列2,返回序列1中去除序列2后剩余的序列

zip

将任意函数应用于两个序列的对应元素

Zip(b, (i, j) => i * j)
SequenceEqual

判断序列对应位置元素是否相等

Concat

连接两个序列

取单个元素

First、FitstOrDefault
  • First:至少有一条,返回第一条。没有则报错
  • FitstOrDefault:取第一条,一条都没有则取默认值

示例

list.First()

list.First(i=>i>3)
Last、LastOrDefault

ElementAt、ElementAtOrDefault

参数为下标,得到指定下标的元素

ElementAtOrDefault:索引超出范围返回默认值(指元素未赋值的缺失值)

Single、SingleOrDefault
  • Single:集合中有且只有一条满足条件的数据,没有则引发异常
  • SingleOrDefault:集合中最多有一条满足条件的数据,找到则返回,找不到返回默认值,多条则报错

示例

list.Single()    //集合中有且只有一条数据,则返回
list.Single(i=>i==5)

添加元素

Prepend、Append

Prepend:向序列开头添加元素,返回新序列

Append:向序列结尾添加元素,返回新序列

排序

OrderBy、OrderByDescending
  • OrderBy:正序(升序)排序,排序规则可任意指定
  • OrderByDescending:倒序(降序)排序

示例

list.OrderBy(i=>i.age)
list.OrderBy(i=>i)

//随机排序
Random rand = new Random();
list.OrderBy(i=> rand.Next());
ThenBy、ThenByDescending

注意:不要写两个OrderBy,否则只会根据最后一个OrderBy排序

IOrderedEnumerable对象才可使用,常跟在OrderBy后面,用来进行多级排序。

示例

list.OrderBy(i=> i.age).ThenBy(i=>i.salary)

序列类型转换

ToArray、ToList

查找元素

Contains
Any
  • 无参时:判断是否为空(容器内是否有元素),为空返回flase,如list。注意不适用于数组,分配空间后即有默认字则为true,除非new int[0]不分配内存空间
  • 单参时,判断集合中是否至少有一条满足条件。Any比Count更高效,类似短路运算,Count需要数完再判断。

示例

items.Any()    //容器内是否包含元素
items.Any(i=>i>3)
ALL

判断是否所有元素都满足条件

分组

GroupBy

返回值序列中的每一组是一个IGrouping对象,IGrouping继承自IEnumerable

参数说明
参数1按照什么进行分组,同时作为回调函数的第一个参数
参数2取对象的哪个属性作为元素,同时作为回调函数的第二个参数
参数3回调函数,同时定义了每个分组可以访问的元素属性

示例

//元素是键值对,按照键值对的值分组
dict.GroupBy(i=>i.Value);

//按对象的年龄字段进行分组,返回类型为IEnumerable<IGrouping<int,Empolyee>>,IGrouping对象的key属性是Age,IGrouping的每个元素是真正的对象
list.GroupBy(i=>i.Age)

//每个元素作为一个分组
GroupBy(i=>i)

//按照是否满足条件分成两组
GroupBy(i => i % 2 == 0)

//按照年龄分组后按照年龄排序,并投影为年龄、人数、平均工资
list.GroupBy(e => e.Age).OrderBy(e => e.Key).Select(g => new { NL = g.Key, RS = g.Count(),  PJ = g.Average(e => e.Salary) });

示例2

static void MyWhere1(IEnumerable<int> list, Func<int,bool> f)
{
    IEnumerable<IGrouping<int, Empolyee>> items = list.GroupBy(e => e.Age);
    foreach(IGrouping<int,Emoloyee> g in items)
    {
        Console.WriteLine(g.Key);
        foreach(Employee e in g)
        {
            Console.WriteLine(e);
        }
    }
}

反转序列

Reverse

LINQ题目

统计一个字符串中每个字母出现的频率(忽略大小写),然后按照从高到低的顺序输出出现频率高于2次的字母和其出现的频率

//先拿到字符串中的所有字母,并转换为小写
//按照每个字母分组
//每个分组取统计项,字母名、数量
//按照数量从高到低排序
//筛选数量大于2的数据
str.Where(c => char.IsLetter(c)).Select(c => char.ToLower(c)).GroupBy(c => c)
    .Select(g => new { g.Key, Count = g.Count() })
    .OrderByDescending(g => g.Count).Where(g => g.Count > 2);