C#学习(一)委托的概念和使用

前言

C#学习系列是根据我以前的笔记整理出来复习,顺便发一下文章做个记录。

先引用一个介绍:

C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。 委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。

根据使用其他语言的经验,在Python和js中,函数(方法)也是一种类型,可以赋值给变量,然后通过变量来调用。C#里也差不多是这个道理,不过C#的委托可以存入多个函数(方法),当调用这个委托的时候同时调用多个方法,这个叫做多播委托。委托具体的实现细节我还没了解,等接下来的学习中继续深入哈~

代码例子

using System;

namespace DelegateAndEvent
{
    class Program
    {
        delegate void voidDelegate();

        delegate int intDelegate();

        static void Main(string[] args)
        {
            // 定义一个多播委托
            voidDelegate d1 = T1;
            d1 += T2;
            d1 += T3;
            d1 += T4;

            // 执行委托
            // 会同时执行t1,t2,t3,t4四个方法
            d1();
        }

        static void T1()
        {
            Console.WriteLine("1");
        }

        static void T2()
        {
            Console.WriteLine("2");
        }

        static void T3()
        {
            Console.WriteLine("3");
        }

        static void T4()
        {
            Console.WriteLine("4");
        }

        static int Q1()
        {
            return 1;
        }

        static int Q2()
        {
            return 2;
        }

        static int Q3()
        {
            return 3;
        }
    }
}

一点理解

要理解委托,得先理解C#的类型系统,CLR支持两种基本类型,值类型和引用类型。下面这张图就可以比较清楚的看到C#的类型系统,大部分同学都是只学过Java的,可能没有深入学过C语言或C++,C#之所以能在各种场景下保持高性能(比如游戏引擎这种计算密集场景),很大程度就是优秀的类型系统设计。

值类型和引用类型介绍

值类型(Value Type),值类型实例通常分配在线程的堆栈(stack)上,并且不包含任何指向实例数据的指针,因为变量本身就包含了其实例数据。其在 MSDN 的定义为值类型直接包含它们的数据,值类型的实例要么在堆栈上,要么内联在结构中。我们由上图可知,值类型主要包括简单类型、结构体类型和枚举类型等。通常声明为以下类型:int、char、float、long、bool、double、struct、enum、short、byte、decimal、sbyte、uint、ulong、ushort 等时,该变量即为值类型。

引用类型(Reference Type),引用类型实例分配在托管堆(managed heap)上,变量保存了实例数据的内存引用。其在 MSDN 中的定义为引用类型存储对值的内存地址的引用,位于堆上。我们由上图可知,引用类型可以是自描述类型、指针类型或接口类型。而自描述类型进一步细分成数组和类类型。类类型是则可以是用户定义的类、装箱的值类型和委托。通常声明为以下类型:class、interface、delegate、object、string 以及其他的自定义引用类型时,该变量即为引用类型。

内存分配

数据在内存中的分配位置,取决于该变量的数据类型。由上可知,值类型通常分配在线程的堆栈上,而引用类型通常分配在托管堆上,由 GC 来控制其回收。如下图:

我们知道,每个变量或者程序都有其堆栈,不同的变量不能共有同一个堆栈地址,因此 myStruct 和myStruct2 在堆栈中一定占用了不同的堆栈地址,尽管经过了变量的传递,实际的内存还是分配在不同的地址上,如果我们再对 myStruct2 变量改变时,显然不会影响到 myStruct 的数据。从图中我们还可以显而易见的看出,myStruct 在堆栈中包含其实例数据,而 myClass 在堆栈中只是保存了其实例数据的引用地址,实际的数据保存在托管堆中。因此,就有可能不同的变量保存了同一地址的数据引用,当数据从一个引用类型变量传递到另一个相同类型的引用类型变量时,传递的是其引用地址而不是实际的数据,因此一个变量的改变会影响另一个变量的值。从上面的分析就可以明白的知道这样一个简单的道理:值类型和引用类型在内存中的分配区别是决定其应用不同的根本原因,由此我们就可以很容易的解释为什么参数传递时,按值传递不会改变形参值,而按址传递会改变行参的值,道理正在于此。

对于内存分配的更详细位置,可以描述如下:

  • 值类型变量做为局部变量时,该实例将被创建在堆栈上;而如果值类型变量作为类型的成员变量时,它将作为类型实例数据的一部分,同该类型的其他字段都保存在托管堆上,这点我们将在接下来的嵌套结构部分来详细说明。

  • 引用类型变量数据保存在托管堆上,但是根据实例的大小有所区别,如下:如果实例的大小小于 85000Byte 时,则该实例将创建在 GC 堆上;而当实例大小大于等于 85000byte 时,则该实例创建在 LOH(Large Object Heap)堆上。

写在后面

OK,大概就写到这里,明天继续学习~

欢迎交流

交流问题请在微信公众号后台留言,每一条信息我都会回复哈~ - 微信公众号:画星星高手 - 打代码直播间:https://live.bilibili.com/11883038 - 知乎:https://www.zhihu.com/people/dealiaxy - 简书:https://www.jianshu.com/u/965b95853b9f