Linq的背后--接口、泛型和委托

本文介绍Linq背后涉及的c#语法和性质

接口

接口,interface,在cpp、c中不存在的一种语法,它是c#的灵魂之一。很多新手不会使用接口,很多时候他们也用不到接口,但是熟练的使用接口是步入进阶的标志之一。接口并不复杂,只要你不抵触新的知识,我保证只要读完这篇文章就能看懂。
所谓接口,即是一种‘约定’。它是编程者设定的,对于类的约定。通过接口我们可以强制所有继承这个接口的类必须实现接口所规定的功能。
例如,我们定义下方的接口:

public interface ICalculateAble
{
    int Calculate(int x, int y);
}

这个接口就规定了,所有的继承它的类需要有一个叫做Calculate的方法,因为这里仅仅是约定,所以接口里的方法是没有方法体的c#8.0中允许写默认方法体,不过这是后话
因此,如果我自己写了一个继承它的class,那么就必须像下边一样:

public class Add: ICalculateAble
{
    public int Calculate(int x, int y)
    {
        // 加法
        return x + y;
    }
}
public class Multiply: ICalculateAble
{
    public int Calculate(int x, int y)
    {
        // 乘法
        return x * y;
    }
}

注意,接口是不能实例化的,因为它知识一个约定。所以我们只能:

ICalculateAble cal1 = new Add();
ICalculateAble cal2 = new Multiply();

然后,我们可以写一个接收ICalculateAble类型参数的方法:

public class Program
{
    public static int DoCalculation(ICalculateAble calculateAble)
    {
        return calculateAble.Calculate(1, 2);
    }
    public static void Main()
    {
        ICalculateAble cal1 = new Add();
        ICalculateAble cal2 = new Multiply();
        var re1 = DoCalculation(cal1);// re1 = 1 + 2 = 3
        var re2 = DoCalculation(cal2);// re2 = 1 * 2 = 2
    }
}

接口作为数据类型的意义是:该数据必须符合这个接口的约定,在上方的意义就是必须具有Calculate方法。
接口的优势就是,增加方法的泛用性。接收接口类型参数的方法,实际上可以接收任何实现该接口的类。而如果接收一个类作为参数,那么这个参数就只能传入那个特定的类了。 另外接口可以用于重构代码。在你觉得自己以前写的代码太差了,要重写的时候,为了防止重写完代码跑不起来,你可以从原本的类中导出一个接口,然后让你重写的类继承这个接口,这样你重写完的类就必定可以完全替代之前的类。
所以在写代码的时候,应当尽可能多的使用接口。

最后,介绍一下linq中的接口IEnumerable,这个接口很常用,它的特点就是:所有实现IEnumerable接口的类可以被foreach遍历,反之不然。这本质是一个语法糖,有兴趣的同学可以自己研究一下。

泛型

所谓泛型Generic,即广泛的类型。
它和利用object强制类型转换、以及一些弱类型语言的自动转换类型不一样,泛型代码在编译后会生成和非泛型一样的IL码。所以泛型对于运行性能是没有影响的。
c#中泛型的语法很简单,比cpp简单的多。我们可以把泛型按照作用范围分为两种:

  • 类泛型

  • 方法泛型

类泛型和方法泛型

public class Generic<T>
{
    public void GenericMethod(T t)
    {
        Console.WriteLine(T);
    }
    public void ForeachPrintMethod<TEnumerable>(TEnumerable t) where TEnumerable:IEnumerable<T>
    {
        foreach(var item in t)
        {
            Console.WriteLine(item);
        }
    }
}

这里设置了一个类泛型T,类里的方法就可以使用T作为类型,和使用普通的class一样。实例化的时候,我们把T替换为我们想用的类型即可:

var g = new Generic<String>();
g.GenericMethod("你好!");//打印你好

然后,注意第二个方法,这里用了一个方法泛型TEnumerable,这个泛型只能在这个方法内使用。同时我们用了一个对该泛型的条件限制:where TEnumerable:IEnumerable<T>这里限制了只有实现(另一种说法继承)了IEnumerable<T>接口的类可以作为该方法的泛型。所以应当这样使用:

var g = new Generic<String>();
g.GenericMethod("你好!");//打印你好
var list = new List<String>();
list.Add("你好1");
list.Add("你好2");
g.ForeachPrintMethod<List<String>>(list);//打印 你好1\n你好2

泛型如果不结合限制条件,那么基本只在链表一类的数据结构里用处大。只有结合了限制条件,才具有灵魂。希望同学们仔细理解这一块,有问题一定要提出。

委托

委托delegate,也是一种约定。不同的是它是对于方法的约定。它类似于cpp中的函数指针。
c#中内置了几十种默认的泛型委托,它们分为两大系,Action<T1,T2,...>Func<T1,T2,...>,这里我主要介绍这两个系的委托的使用。

  • Action<T1,T2,...>系是没有返回值的方法。也就是说返回void它的所有泛型是方法的参数的类型,有多少参数就有多少个泛型。

  • Func<T1,T2,...>系是有返回值的方法。它的返回值类型是最后一个泛型的类型,其它的泛型是方法参数的类型。

举例:

public class Program
{
    public static int DoSomething(int input1, string input2)
    {
        Console.WriteLine("Something");
        return 3;
    }
    public static void DoNothing(int input1, string input2)
    {
        Console.WriteLine("Nothing");
    }
    public static void Main()
    {
        Action<int, String> act = DoNothing;
        Func<int, String, int> func = DoSomething;
        act(1,"1");//输出 Nothong
        var i = func(1,"1");//输出 Something, i=3
    }
}


实际情况中我们常常使用lambda表达式构造匿名方法:

public static void Main()
{
    Action<int, String> act = (i1,s1)=>//i1和s1是形参,不需要加参数类型,编译器会自动推测
    {
        i1++;
        Console.WriteLine(s1);
    };//多行lambda表达式
    Func<int, String, int> func = (i1,s1)=>i1++;//方法体只有1行的lambda的简写方法,这一行的计算结果就是返回值
    act(1,"1");//输出 1
    var i = func(1,"1");//没输出, i=2
}

牢牢掌握lambda对使用linq有很大帮助。


本文章使用limfx的vsocde插件快速发布