c#委托、定时器、反射和面向对象

本文深入探讨了C#中的委托、Action与Func的使用,详细解析了三种定时器的实现方式,阐述了抽象类与接口的区别,以及反射机制的应用,为C#开发者提供了丰富的实践指导。

一 Delegate

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

1 声明委托(Delegate)

  委托声明决定了可由该委托引用的方法。委托可指向一个与其具有相同标签的方法。例如,假设有一个委托:

public delegate int MyDelegate (string s);

  上面的委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量。声明委托的语法如下:

delegate <return type> <delegate-name> <parameter list>

2 实例化委托(Delegate)

  一旦声明了委托类型,委托对象必须使用 new 关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new 语句的参数就像方法调用一样书写,但是不带有参数。例如:

public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);

  下面的实例演示了委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。实例:

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      static int num = 10;
      public static int AddNum(int p)
      {
         num += p;
         return num;
      }

      public static int MultNum(int q)
      {
         num *= q;
         return num;
      }
      public static int getNum()
      {
         return num;
      }

      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         // 使用委托对象调用方法
         nc1(25);
         Console.WriteLine("Value of Num: {0}", getNum());
         nc2(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}
/*
//当上面的代码被编译和执行时,它会产生下列结果:
Value of Num: 35
Value of Num: 175
*/

3 Func与Action的理解

  ActionFunc 是.NET类库中增加的内置委托,以便更加简洁方便的使用委托。
  最初使用委托时,均需要先定义委托类型,然后定义一个符合委托类型签名的函数,在调用前,需声明并创建委托对象,将指定函数与委托进行关联。
  例1:

public delegate int Math(int param1, int param2);定义委托类型
public int Add(int param1, int param2)//定义同签名函数
{
	return param1 + param2;
}
Math math;//声明委托
math = new Math(Add); //创建委托对象,与指定进行关联
math(3, 4);//调用委托函数

  如果需要三个、四个参数的委托类型,则需要再次定义委托类型。简单的委托调用,却需要根据签名不断改变多次定义委托类型,而微软推出了对此进行简化的内置委托类型:ActionFunc,简化了这些不必要的操作。
  内置委托类型,顾名思义 ActionFunc 本身就是已经定义好的委托类型。两种委托类型的区别在于:Action 委托签名不提供返回类型,而 Func 提供返回类型。
  Action 委托具有 Action<T>Action<T1, T2>Action<T1, T2, T3>……Action<T1, ……T16> 多达16个的重载,其中传入参数均采用泛型中的类型参数T,涵盖了几乎所有可能存在的无返回值的委托类型。Func 则具有 Func<TResult>Func<T, Tresult>……Func<T1, T2, T3……, Tresult> 17种类型重载,T1……T16为出入参数,Tresult 为返回类型。
  例1通过简单改造:

Func<int, int, int> math = Add;//指定委托对象并关联函数
math(3,4);//调用委托函数

  无需定义直接进行声明关联。既然是委托类型,也同样可以与匿名函数、或者采用Lambda表达式结合使用:
  匿名函数:

Func<int, int, int> math=delegate(int param1, int param2)
{
	return param1 + param2;
}

  Lambda:

Func<int, int, int> math=(param1, param2)=>
{
	return param1 + param2;
}

  Action 的使用如同上面 Func 的使用一样,只是缺少了返回类型,直接调用委托函数。

public void Add(int param1, int param2)
{
	MessageBox.show((param1 + param2).ToString());
}

  遇到此类的委托函数调用,那就可以直接用 Action 了:

Action<int, int> math = Add;
math(3,4);

4 Action 带参数的委托方法使用,并赋值操作

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ActionArmy : MonoBehaviour
{
    public static void CreateArmy(GameObject go, Action<ActionArmy> callback)
    {
        //安全校验
        if (go != null)
        {
            //参数操作
            ActionArmy army = go.AddComponent<ActionArmy>();
            //校验
            if (callback != null)
            {
                //回调方法
                callback(army);
            }
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Army : MonoBehaviour
{
    //私有对象
    ActionArmy actionArmy;
    public GameObject go;
	void Start ()
	{
        //调用创建对象,并赋值给actionArmy
        ActionArmy.CreateArmy(go,(army)=>{actionArmy=army;});
	}
}

5 Action 和 =>的用法

  经常出现 Action=> 结合的函数…百度了好久,要么是讲=>的。要么是讲 Action。单独看是理解,一弄在一起我就混乱了…
  例子如下:

class Program
{
    public static void Start()
    {
        test((str) => {
        	//3
            Console.WriteLine(str);
        });

    }
    public static void test(Action<string> loaded)
    {
    	//1
        Console.WriteLine("235466");
        //2
        loaded("我是我啊");
        //4
        Console.WriteLine("那我是谁");

    }
    static void Main(string[] args)
    {
        Start();
    }
}
//运行结果
/*
235466
我是我啊
那我是谁
*/

  显而易见,test 函数输入为一个 lambda 表达式,也就是一个函数调用。

6 Action和Func的使用示例

  本节内容来自这里

private void button1_Click(object sender, EventArgs e)
{
    //报错:从不是创建控件的线程访问它
    Thread t = new Thread(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            this.label1.Text = i.ToString();
        }
    });
    t.Start();
}

  上面的代码会报错,从新的线程访问其他线程的控件:这是因为.NET禁止了跨线程调用控件, 否则谁都可以操作控件,最后可能造成错误。
  1:线程间操作无效: 从不是创建控件的线程访问它。那么创建它的线程是哪个线程?
  (百度答案)从程序设计上来说,只有创建界面的主线程才能访问界面上的控件,所以会出错.
  当然,设置 CheckForIllegalCrossThreadCalls = false 是能去掉这个检查的,但是这不是标准的作法.
  标准的作法是在访问界面控件时,访问下窗体的 InvokeRequired 属性,如果为 false 就可以直接访问,否则就是跨线程访问;此时,创建一个 delegate,并通过 Invoke() 来调用它.

private void button1_Click(object sender, EventArgs e)
{
    //线程的依附性:只有创建某些对象的线程、才能访问它所创建的某些对象
    //解决跨线程调用的问题,可以采用封送(Marshal) .调用Invoke方法
    //使用封送
    new Thread(() =>
    {
        for (int i = 0; i < 10000; i++)
        {
            Action<int> action = (data) =>
            {
                this.label1.Text = data.ToString(); };
                Invoke(action,i);
            }
    }).Start();
    //MessageBox 没有遵循依附性的原则,所以可以在工作线程中直接访问
}

  2:为什么用了上面的代码之后就不报错了?
  2.1 将 Action 转到定义发现

namespace System
{
    // 摘要:
    //     封装一个方法,该方法只有一个参数并且不返回值。
    //
    // 参数:
    //   obj:
    //     此委托封装的方法的参数。
    //
    // 类型参数:
    //   T:
    //     此委托封装的方法的参数类型。
    public delegate void Action<in T>(T obj);
}

  2.2 Action 是个委托:那么自己来写个委托(对于自己未掌握的知识,我习惯用自己写的代码)

//申明一个委托对象
public delegate void Action2<in T>(T t);
private void button1_Click(object sender, EventArgs e)
{
    new Thread(() =>
    {
        for (int i = 0; i < 10000; i++)
        {
            Action2<int> a = new Action2<int>(Action2Test);
            Invoke(a, i);
        }
    }).Start();
    
}
public void Action2Test(int t)
{
    this.label1.Text = t.ToString();
}

  简言之,委托将参数与方法传递给控件所在的线程,并由控件所在的线程执行。

二 C#三种定时器的实现

  在C#里关于定时器类就有3个:
  1、基于 Windows 的标准计时器(System.Windows.Forms.Timer);
  2、基于服务器的计时器(System.Timers.Timer);
  3、线程计时器(System.Threading.Timer)
  System.Windows.Forms.Timer是应用于WinForm中的,它是通过Windows消息机制实现的,类似于VB或Delphi中的Timer控件,内部使用API SetTimer实现的。它的主要缺点是计时不精确,而且必须有消息循环,Console Application(控制台应用程序)无法使用。
  System.Timers.TimerSystem.Threading.Timer非常类似,它们是通过.NET Thread Pool实现的,轻量,计时精确,对应用程序、消息没有特别的要求。System.Timers.Timer还可以应用于WinForm,完全取代上面的 Timer 控件。它们的缺点是不支持直接的拖放,需要手工编码。

//使用System.Timers.Timer类
System.Timers.Timer t = new System.Timers.Timer(10000);//实例化Timer类,设置间隔时间为10000毫秒;
t.Elapsed += new System.Timers.ElapsedEventHandler(theout);//到达时间的时候执行事件;
t.AutoReset = true;//设置是执行一次(false)还是一直执行(true);
t.Enabled = true;//是否执行System.Timers.Timer.Elapsed事件;
public void theout(object source, System.Timers.ElapsedEventArgs e)
{
	MessageBox.Show("OK!");
}

1 System.Windows.Forms.Timer

  首先注意一点就是:Windows 计时器是为单线程环境设计的,此计时器从Visual Basic 1.0 版起就存在于该产品中,并且基本上未做改动,这个计时器是使用最简单的一种,只要把工具箱中的Timer控件拖到窗体上,然后设置一下事件和间隔时间等属性就可以了。
  实验出来的结果也完全符合单线程的特点:
  1、当启动此计时器后,会在下方子线程ID列表中显示子线程ID,并且和主线程ID相同;

private void formsTimer_Tick(object sender, EventArgs e)
{
	i++;
	lblSubThread.Text += "子线程执行,线程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n"}

  2、当单击主线程暂停5秒后,子线程会暂停执行,并且当5秒之后不会执行之前被暂停的子线程,而是直接执行后面的子线程(也就是会少输出几行值);

System.Threading.Thread.Sleep(5000)

  3、在子进程的事件中暂停5秒会导致主窗口相应无响应5秒;
  4、定义一个线程静态变量:

[ThreadStatic]
private static int i = 0

  在子线程事件中每次加一,再点击线程静态变量值会得到增加后的i值。

2 System.Timers.Timer

  System.Timers.Timer不依赖窗体,是从线程池唤醒线程,是传统的计时器为了在服务器环境上运行而优化后的更新版本。
  需要手工编码使用此计时器,使用方式有两种。
  1、通过SynchronizingObject属性依附于窗体

System.Timers.Timer timersTimer = new System.Timers.Timer();
timersTimer.Enabled = false;
timersTimer.Interval = 100;
timersTimer.Elapsed += new System.Timers.ElapsedEventHandler(timersTimer_Elapsed);
timersTimer.SynchronizingObject = this;

private void timersTimer_Elapsed(object sender, ElapsedEventArgs e)
{}

  通过这种方式来使用,实验效果几乎和基于 Windows 的标准计时器一样,只是在上面的第二条实验中,虽然也会暂停子线程的执行,不过在5秒之后把之前排队的任务都执行掉(也就是不会少输出几行值)
  2、不使用SynchronizingObject属性
  这种方式就是多线程的方式了,即启动的子线程和主窗体不在一个线程。不过这样也存在一个问题:由于子线程是单独的一个线程,那么就不能访问住窗体中的控件了,只能通过代理的方式来访问:

delegate void SetTextCallback(string text)void timersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
  //使用代理
  string text = "子线程执行,线程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n";
   SetTextCallback d = new SetTextCallback(SetText);
  this.Invoke(d, new object[] { text });
   i++}
private void SetText(string text)
{
   lblSubThread.Text += text;
}

  这样再次实验就会得到如下的结果:
  1、当启动此计时器后,会在下方子线程ID列表中显示子线程ID,并且和主线程ID不相同
  2、当单击主线程暂停5秒后,子线程会一直往下执行(界面上可能看不出来,不过通过在子线程输出文件的方式可以很方便的看出来)
  3、在子进程的事件中暂停5秒不会导致主窗口无响应
  4、在子线程事件中每次给线程静态变量加一,再点击线程静态变量值得到的值还是0(不会改变主窗口中的线程静态变量)

3 System.Threading.Timer

  线程计时器也不依赖窗体,是一种简单的、轻量级计时器,它使用回调方法而不是使用事件,并由线程池线程提供支持。
  对消息不在线程上发送的方案中,线程计时器是非常有用的。
  使用方法如下:

System.Threading.Timer threadTimer;
public void ThreadMethod(Object state)
{
	//使用代理
	string text = "子线程执行,线程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n"SetTextCallback d = new SetTextCallback(SetText)this.Invoke(d, new object[] { text });
	i++}
private void Form1_Load(object sender, EventArgs e)
{
	threadTimer = new System.Threading.Timer(new System.Threading.TimerCallback(ThreadMethod)null-1-1)}

  暂停代码:

threadTimer.Change(-1-1)

  实验的效果和基于服务器的计时器(System.Timers.Timer)的第二种方式是一样的,当然具体的使用方法和原理是不一样的,最主要的就是这种方式使用的是代理的方式而不是事件的方式,并且可以不依赖于窗体和组件而单独执行。
  下面列出老外总结的一张表(三种方式的区别):

Feature descriptionSystem.Timers.TimerSystem.Threading.TimerSystem.Windows.Forms.Timer
Support for adding and removing listeners after the timer is instantiated.YesNoYes
Supports call backs on the user-interface threadYesNoYes
Calls back from threads obtained from the thread poolYesYesNo
Supports drag-and-drop in the Windows Forms DesignerYesNoYes
Suitable for running in a server multi-threaded environmentYesYesNo
Includes support for passing arbitrary state from the timer initialization to the callback.NoYesNo
Implements IDisposableYesYesYes
Supports one-off callbacks as well as periodic repeating callbacksYesYesYes
Accessible across application domain boundariesYesYesYes
Supports IComponent – hostable in an IContainerYesNoYes

三 抽象类和接口的区别

1 面向接口编程和面向对象编程是什么关系

  首先,面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其一部分。或者说,它是面向对象编程体系中的思想精髓之一。

2 接口的本质

  接口,在表面上是由几个没有主体代码的方法定义组成的集合体,有唯一的名称,可以被类或其他接口所实现(或者也可以说继承)。它在形式上可能是如下的样子:

interface InterfaceName
{
     void Method1();
     void Method2(int para1);
     void Method3(string para2,string para3);
}

  接口的本质是什么呢?或者说接口存在的意义是什么。认为:
  **1)**接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。体现了自然界“如果你是……则必须能……”的理念。
   例如,在自然界中,人都能吃饭,即“如果你是人,则必须能吃饭”。那么模拟到计算机程序中,就应该有一个 IPerson(习惯上,接口名由“I”开头)接口,并有一个方法叫 Eat(),然后规定,每一个表示“人”的类,必须实现 IPerson接口,这就模拟了自然界“如果你是人,则必须能吃饭”这条规则。
  从这里,我想各位也能看到些许面向对象思想的东西。面向对象思想的核心之一,就是模拟真实世界,把真实世界中的事物抽象成类,整个程序靠各个类的实例互相通信、互相协作完成系统功能,这非常符合真实世界的运行状况,也是面向对象思想的精髓。
  **2)**接口是在一定粒度视图上同类事物的抽象表示。注意这里我强调了在一定粒度视图上,因为“同类事物”这个概念是相对的,它因为粒度视图不同而不同。
   例如,在我的眼里,我是一个人,和一头猪有本质区别,我可以接受我和我同学是同类这个说法,但绝不能接受我和一头猪是同类。但是,如果在一个动物学家眼里,我和猪应该是同类,因为都是动物,他可以认为“人”和“猪”都实现了 IAnimal这个接口,而他在研究动物行为时,不会把我和猪分开对待,而会从“动物”这个较大的粒度上研究,但他会认为我和一棵树有本质区别。
  现在换了一个遗传学家,情况又不同了,因为生物都能遗传,所以在他眼里,我不仅和猪没区别,和一只蚊子、一个细菌、一颗树、一个蘑菇乃至一个SARS病毒都没什么区别,因为他会认为都实现了 IDescendable这个接口,即都是可遗传的东西,他不会分别研究,而会将所有生物作为同类进行研究,在他眼里没有人和病毒之分,只有可遗传的物质和不可遗传的物质。但至少,我和一块石头还是有区别的。
  可不幸的事情发生了,某日,地球上出现了一位伟大的人,他叫列宁,他在熟读马克思、恩格斯的辩证唯物主义思想巨著后,颇有心得,于是他下了一个著名的定义:所谓物质,就是能被意识所反映的客观实在。至此,我和一块石头、一丝空气、一条成语和传输手机信号的电磁场已经没什么区别了,因为在列宁的眼里,都是可以被意识所反映的客观实在。如果列宁是一名程序员,他会这么说:所谓物质,就是所有同时实现了 IReflectabeIEsse 两个接口的类所生成的实例。
  也许你会觉得我上面的例子像在瞎掰,但是,这正是接口得以存在的意义。面向对象思想核心之一叫做多态性,什么叫多态性?说白了就是在某个粒度视图层面上对同类事物不加区别的对待而统一处理。而之所以敢这样做,就是因为有接口的存在。像那个遗传学家,他明白所有生物都实现了 IDescendable 接口,那只要是生物,一定有 Descend() 这个方法,于是他就可以统一研究,而不至于分别研究每一种生物而最终累死。

3 面向接口编程综述

  那么什么是面向接口编程呢?我个人的定义是:在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类。
  这样做的好处是显而易见的,首先对系统灵活性大有好处。当下层需要改变时,只要接口及接口功能不变,则上层不用做任何修改。甚至可以在不改动上层代码时将下层整个替换掉,就像将一个WD的60G硬盘换成一个希捷的160G的硬盘,计算机其他地方不用做任何改动,而是把原硬盘拔下来、新硬盘插上就行了,因为计算机其他部分不依赖具体硬盘,而只依赖一个IDE接口,只要硬盘实现了这个接口,就可以替换上去。从这里看,程序中的接口和现实中的接口极为相似,所以我一直认为,接口(interface)这个词用的真是神似!
  使用接口的另一个好处就是不同部件或层次的开发人员可以并行开工,就像造硬盘的不用等造CPU的,也不用等造显示器的,只要接口一致,设计合理,完全可以并行进行开发,从而提高效率。

4 对本文的补充

  1、关于“面向接口编程”中的“接口”与具体面向对象语言中“接口”两个词
  看到有朋友提出“面向接口编程”中的“接口”二字应该比单纯编程语言中的 interface 范围更大。面向对象语言中的“接口”是指具体的一种代码结构,例如C#中用 interface 关键字定义的接口。而“面向接口编程”中的“接口”可以说是一种从软件架构的角度、从一个更抽象的层面上指那种用于隐藏具体底层类和实现多态性的结构部件。从这个意义上说,如果定义一个抽象类,并且目的是为了实现多态,那么我认为把这个抽象类也称为“接口”是合理的。但是用抽象类实现多态合理不合理?在下面第二条讨论。
  概括来说,我觉得两个“接口”的概念既相互区别又相互联系。“面向接口编程”中的接口是一种思想层面的用于实现多态性、提高软件灵活性和可维护性的架构部件,而具体语言中的“接口”是将这种思想中的部件具体实施到代码里的手段。
  2、关于抽象类与接口
  如果单从具体代码来看,对这两个概念很容易模糊,甚至觉得接口就是多余的,因为单从具体功能来看,除多重继承外(C#,Java中),抽象类似乎完全能取代接口。抽象类和接口的区别在于使用动机。使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。所以,如果你在为某个地方该使用接口还是抽象类而犹豫不决时,那么可以想想你的动机是什么。
  看到有朋友对 IPerson 这个接口的质疑,我个人的理解是,IPerson 这个接口该不该定义,关键看具体应用中是怎么个情况。如果的项目中有 WomenMan,都继承 Person,而且 WomenMan 绝大多数方法都相同,只有一个方法 DoSomethingInWC() 不同(例子比较粗俗,各位见谅),那么当然定义一个 abstract Person 抽象类比较合理,因为它可以把其他所有方法都包含进去,子类只定义DoSomethingInWC(),大大减少了重复代码量。
  但是,如果程序中的 WomenMan 两个类基本没有共同代码,而且有一个 PersonHandle 类需要实例化他们,并且不希望知道他们是男是女,而只需把他们当作人看待,并实现多态,那么定义成接口就有必要了。
  总而言之,接口与抽象类的区别主要在于使用的动机,而不在于其本身。而一个东西该定义成抽象类还是接口,要根据具体环境的上下文决定。
  再者,我认为接口和抽象类的另一个区别在于,抽象类和它的子类之间应该是一般和特殊的关系,而接口仅仅是它的子类应该实现的一组规则。(当然,有时也可能存在一般与特殊的关系,但使用接口的目的不在这里)如,交通工具定义成抽象类,汽车、飞机、轮船定义成子类,是可以接受的,因为汽车、飞机、轮船都是一种特殊的交通工具。再譬如 Icomparable 接口,它只是说,实现这个接口的类必须要可以进行比较,这是一条规则。如果 Car 这个类实现了 Icomparable,只是说,的 Car 中有一个方法可以对两个 Car 的实例进行比较,可能是比哪辆车更贵,也可能比哪辆车更大,这都无所谓,但不能说“汽车是一种特殊的可以比较”,这在文法上都不通。

四 抽象类

  抽象类不能被实例化,抽象方法不能有方法体,抽象类中的所有抽象方法必须在子类中重写(override ),一个抽象类可以同时包含抽象方法和非抽象方法。
  abstract 修饰符可以和类、方法、属性、索引器及事件一起使用。在类声明中使用 abstract 修饰符以指示类只能是其他类的基类。

1 重载

  重载(overload):在同一个作用域(一般指一个类)的两个或多个方法函数名相同,参数列表不同的方法叫做重载,它们有三个特点(俗称两必须一可以):方法名必须相同;参数列表必须不相同;返回值类型可以不相同。

2 重写

  重写(override):子类中为满足自己的需要来重复定义某个方法的不同实现,需要用override关键字,被重写的方法必须是虚方法,用的是virtual关键字。它的特点是(三个相同):相同的方法名;相同的参数列表;相同的返回值。
  如:父类中的定义:

public virtual void EatFood()
{
    Console.WriteLine("Animal吃东西");
} 

  子类中的定义:

public override void EatFood()
{
    Console.WriteLine("Cat吃东西");
    //base.EatFood();
}

3 虚方法

  基类中定义的允许在派生类中重写的方法,使用virtual关键字定义。如:

public virtual void EatFood()
{
	Console.WriteLine("Animal吃东西");
}

  注意:虚方法也可以被直接调用。如:

Animal a = new Animal();
a.EatFood();

  virtual 关键字:virtual 关键字在基类中修饰方法,会出现两种情况(不一定要在抽象类中,实体类也是可以用这关键字的)
  1、在基类中定义了 virtual 方法,在派生类中没有重写该虚方法,那么派生类实例的调用中,该虚方法使用的是基类定义的方法。
  2、在基类中定义了 virtual 方法,在派生类中重写了该虚方法,那么在对派生类实例的调用中,该虚方法使用的是派生类重写的方法。

4 抽象方法

  在基类中定义的并且必须在派生类中重写的方法,使用 abstract 关键字定义。如:

public abstract class Biology
{
    public abstract void Live();
}
public class Animal : Biology
{
    public override void Live()
    {
        Console.WriteLine("Animal重写的抽象方法");
        //throw new NotImplementedException();
    } 
}

  注意:抽象方法只能在抽象类中定义,如果不在抽象类中定义,则会报错。
  虚方法和抽象方法的区别是:因为抽象类无法实例化,所以抽象方法没有办法被调用,也就是说抽象方法永远不可能被实现。

5 隐藏方法

  在派生类中定义的和基类中的某个方法同名的方法,使用 new 关键字定义。如在基类 Animal 中有一方法 Sleep()

public void Sleep()
{
	Console.WriteLine("Animal Sleep");
}

  则在派生类Cat中定义隐藏方法的代码为:

new public void Sleep()
{
	Console.WriteLine("Cat Sleep");
}

  或者为:

public new void Sleep()
{
	Console.WriteLine("Cat Sleep");
}    

注意:
  (1)隐藏方法不但可以隐藏基类中的虚方法,而且也可以隐藏基类中的非虚方法。
  (2)隐藏方法中父类的实例调用父类的方法,子类的实例调用子类的方法。
  (3)和上一条对比:重写方法中子类的变量调用子类重写的方法,父类的变量要看这个父类引用的是子类的实例还是本身的实例,如果引用的是父类的实例那么调用基类的方法,如果引用的是派生类的实例则调用派生类的方法。

6 示例

public abstract class Biology
{
	public abstract void Live();
}
public class Animal : Biology
{
  public override void Live()
  {
      Console.WriteLine("Animal重写的Live");
      //throw new NotImplementedException();
  }
  public void Sleep()
  {
      Console.WriteLine("Animal Sleep");
  }
  public int Sleep(int time)
  {
      Console.WriteLine("Animal在{0}点Sleep", time);
      return time;
  }
  public virtual void EatFood()
  {
      Console.WriteLine("Animal EatFood");
  }
}
public class Cat : Animal
{
  public override void EatFood()
  {
      Console.WriteLine("Cat EatFood");
      //base.EatFood();
  }
  new public void Sleep()
  {
      Console.WriteLine("Cat Sleep");
  }
  //public new void Sleep()
  //{
  //    Console.WriteLine("Cat Sleep");
  //}
}
public class Dog : Animal
{
  public override void EatFood()
  {
      Console.WriteLine("Dog EatFood");
      //base.EatFood();
  }
}

  下面来看看需要执行的代码:

class Program
{
	static void Main(string[] args)
	{
	    //Animal的实例
	    Animal a = new Animal();
	    //Animal的实例,引用派生类Cat对象
	    Animal ac = new Cat();
	    //Animal的实例,引用派生类Dog对象
	    Animal ad = new Dog();
	    //Cat的实例
	    Cat c = new Cat();
	    //Dog的实例
	    Dog d = new Dog();
	    //重载
	    a.Sleep();
	    a.Sleep(23);
	    //重写和虚方法
	    a.EatFood();
	    ac.EatFood();
	    ad.EatFood();
	    //抽象方法
	    a.Live();
	    //隐藏方法
	    a.Sleep();
	    ac.Sleep();
	    c.Sleep();
	    Console.ReadKey();
	}
}

7 深入理解多态性

  要深入理解多态性,就要先从值类型和引用类型说起。都知道值类型是保存在线程栈上的,而引用类型是保存在托管堆中的。因为所有的类都是引用类型,所以仅仅看引用类型。
  现在回到刚才的例子,Main函数时程序的入口,在JIT编译器将Main函数编译为本地CPU指定时,发现该方法引用了Biology、Animal、Cat、Dog这几个类,所以CLR会创建几个实例来表示这几个类型本身,把它称之为“类型对象”。该对象包含了类中的静态字段,以及包含类中所有方法的方法表,还包含了托管堆中所有对象都要有的两个额外的成员——类型对象指针(Type Object Point)和同步块索引(sync Block Index)。
  可能上面这段对于有些没有看过相关CLR书籍的童鞋没有看懂,所以画个图来描述一下:
在这里插入图片描述
  上面的这个图是在执行Main函数之前CLR所做的事情,下面开始执行Main函数(方便起见,简化一下Main函数):

//Animal的实例
Animal a = new Animal();
//Animal的实例,引用派生类Cat对象
Animal ac = new Cat();
//Animal的实例,引用派生类Dog对象
Animal ad = new Dog();
a.Sleep();
a.EatFood();
ac.EatFood();
ad.EatFood();

  下面实例化三个Animal实例,但是他们实际上指向的分别是Animal对象、Cat对象和Dog对象,如下图:
在这里插入图片描述
  请注意,变量ac和ad虽然都是Animal类型,但是指向的分别是Cat对象和Dog对象,这里是关键。
  当执行a.Sleep()时,由于Sleep是非虚实例方法,JIT编译器会找到发出调用的那个变量(a)的类型(Animal)对应的类型对象(Animal类型对象)。然后调用该类型对象中的Sleep方法,如果该类型对象没有Sleep方法,JIT编译器会回溯类的基类(一直到Object)中查找Sleep方法。
  当执行ac.EatFood时,由于EatFood是虚实例方法,JIT编译器调用时会在方法中生成一些额外的代码,这些代码会首先检查发出调用的变量(ac),然后跟随变量的引用地址找到发出调用的对象(Cat对象),找到发出调用的对象对应的类型对象(Cat类型对象),最后在该类型对象中查找EatFood方法。同样的,如果在该类型对象中没有查找到EatFood方法,JIT编译器会回溯到该类型对象的基类中查找。
  上面描述的就是JIT编译器在遇到调用类型的非虚实例方法以及虚实例方法时的不同执行方式,也这是处理这两类方法的不同方式造成了表面上看到的面向对象的三个特征之一——多态性。
  好了,本篇博文开始回顾了一些关于多态性的基本概念,然后解释了多态性的内部机理。内部JIT编译器的部分基本是参照《CLR via C#》书中的第四章的内容,有这本书的同学可以回去翻翻看看。写的不好的地方,请大家批评指正。

五 抽象类和接口有什么区别

  接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。
  在应用上的区别:接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用。模板方法设计模式是抽象类的一个典型应用。

1 抽象类

  (1) 抽象方法只作声明,而不包含实现,可以看成是没有实现体的虚方法;
  (2) 抽象类不能被实例化;
  (3) 抽象类可以但不是必须有抽象属性和抽象方法,但是一旦有了抽象方法,就一定要把这个类声明为抽象类;
  (4) 具体派生类必须覆盖基类的抽象方法;
  (5) 抽象派生类可以覆盖基类的抽象方法,也可以不覆盖。如果不覆盖,则其具体派生类必须覆盖它们。如:

using System;
public abstract class A //抽象类A 
{ 
    private int num=0;
    public int Num //抽象类包含属性 
    { 
        get { return num; } 
        set { num = value; }         
    }
    public virtual int getNum() //抽象类包含虚方法 
    { 
        return num; 
    }
    public void setNum(int n) // //抽象类包含普通方法 
    { 
        this.num = n; 
    }
    public abstract void E(); //类A中的抽象方法E     
}
public abstract class B : A //由于类B继承了类A中的抽象方法E,所以类B也变成了抽象类 
{  
}
public class C : B 
{ 
    public override void E() //重写从类A继承的抽象方法。如果类B自己还定义了抽象方法,也必须重写 
    { 
        //throw new Exception("The method or operation is not implemented."); 
    } 
}
public class Test 
{ 
    static void Main() 
    { 
        C c = new C(); 
        c.E(); 
    } 
}

2 接口

  (1) 接口不能被实例化;
  (2) 接口只能包含方法声明;
  (3) 接口的成员包括方法、属性、索引器、事件;
  (4) 接口中不能包含常量、字段(域)、构造函数、析构函数、静态成员。如:

public delegate void EventHandler(object sender, Event e);
public interface ITest 
{
    int A { get; set; }
    void Test();
    event EventHandler Event;    
    int index { get; set; }
}

  (5) 接口中的所有成员默认为 public,因此接口中不能有 private 修饰符;
  (6) 派生类必须实现接口的所有成员;
  (7) 一个类可以直接实现多个接口,接口之间用逗号隔开;
  (8) 一个接口可以有多个父接口,实现该接口的类必须实现所有父接口中的所有成员。

3 抽象类和接口

  相同点:
  (1) 都可以被继承;
  (2) 都不能被实例化;
  (3) 都可以包含方法声明;
  (4) 派生类必须实现未实现的方法。
  区 别:
  (1) 抽象基类可以定义字段、属性、方法实现。接口只能定义属性、索引器、事件、和方法声明,不能包含字段。
  (2) 抽象类是一个不完整的类,需要进一步细化,而接口是一个行为规范。微软的自定义接口总是后带able字段,证明其是表述一类“我能做。。。”
  (3) 接口可以被多重实现,抽象类只能被单一继承。
  (4) 抽象类更多的是定义在一系列紧密相关的类间,而接口大多数是关系疏松但都实现某一功能的类中。
  (5) 抽象类是从一系列相关对象中抽象出来的概念, 因此反映的是事物的内部共性;接口是为了满足外部调用而定义的一个功能约定, 因此反映的是事物的外部特性。
  (6) 接口基本上不具备继承的任何具体特点,它仅仅承诺了能够调用的方法。
  (7) 接口可以用于支持回调,而继承并不具备这个特点。
  (8) 抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法却默认为非虚的,当然您也可以声明为虚的。
  (9) 如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法。

六 反射

1 能解决什么问题

  反射是一种机制,通过这种机制可以知道一个未知类型的类型信息。通过反射机制可以知道未知类型对象的类型信息。
  再比如,有一个 dll 文件,想调用里面的类。现在假设这个 dll 文件的类的定义,数量等不是固定的,是经常变化的。也许某一天你要在这个 dll 里面增加一个类定义。也许你觉得这没什么问题,现在关键是在另一个程序集里面要调用这个 dll ,这是的程序必须能够适应这个 dll 的变化,也就是说即使改变了 dll 文件的定义也不需要改变的程序集。这时候就会使用一个未知 dll 。该怎么办?同样,反射机制帮助了,可以通过反射来实现。
  说白了,反射就是能知道未知类型的类型信息这么一个东西。没什么神秘可讲!下面是一个获得程序集信息的例子。

2 示例

  下面来举一个例子。 例子的思路是这样的: 有一个 dll。 该 dll 里面有许多关于运动的类。 每一个类记录了一种体育运动的信息。在另外一个程序里面要知道这个 dll 的信息:(如果你还不能明白我的意思,请耐心的照我的步骤把这个过程走一变!)
  第一步:建一个文件 Sport.cs,内容如下:

using System;
public abstract class Sport
{
    protected string name;
    public abstract string GetDuration();
    public abstract string GetName();
}

  咱们用命令 csc /t:library Sport.cs 编译它。
  第二步,再建一个名为 SomeSports.cs 的文件,内容如下:

using System;

public class Football : Sport
{
    public Football()
    {
        name = "Football";
    }
    public override string GetDuration()
    {
        return "four 15 minute quarters";
    }
    public override string GetName()
    {
        return name;
    }
}

public class Hockey : Sport
{
    public Hockey()
    {
        name = "Hockey";
    }
    public override string GetDuration()
    {
        return "three 20 minute periods";
    }
    public override string GetName()
    {
        return name;
    }
}

public class Soccer : Sport
{
    public Soccer()
    {
        name = "Soccer";
    }
    public override string GetDuration()
    {
        return "two 45 minute halves";
    }
    public override string GetName()
    {
        return name;
    }
}

  下面用命令 csc /t:library /r:Sport.dll SomeSports.cs 编译该文件。
  现在有了的运动信息 dll 文件。现在想通过程序知道里面有哪些类。请进入最后一步,第三步:创建文件 AssemblyDemo.cs,内容如下:

using System;
using System.Reflection;

public class AssemblyDemo
{
    public static void Main(string[] args)
    {
        int i, j;
        //==========================
        //First the command line arguments are evaluated.if there isn't
        //at least one,a usage message is printed
        //=================================
        if (args.GetLength(0) < 1)
        {
            Console.WriteLine("usage is AssemblyDemo<library_name>");
        }
        else
        {
            //========================
            // An Assembly object is obtained from the command line argument
            //========================
            Assembly assembly = Assembly.LoadFrom(args[0]);
            Type[] types = assembly.GetTypes();
            Console.WriteLine(assembly.GetName().Name + "contains the following types");
            for (i = 0; i < types.GetLength(0); ++i)
            {
                Console.WriteLine("\r(" + i + ") " + types[i].Name);
            }
            i = types.Length - 1;
            Console.Write("make selection(0-" + i + ");");
            j = Convert.ToInt32(Console.ReadLine());
            Console.WriteLine();
            if (types[j].IsSubclassOf(typeof(Sport)))
            {
                ConstructorInfo ci = types[j].GetConstructor(new Type[0]);
                Sport sport = (Sport)ci.Invoke(new Object[0]);
                Console.WriteLine(sport.GetName() + " has " + sport.GetDuration());
            }
            else
            {
                Console.WriteLine(types[j].Name + " is not a sub-class of Sport");
            }
        }
    }
}

  咱们用命令 csc /r:Sport.dll AssemblyDemo.cs 编译该文件,下面用 AssemblyDemo SomeSports.dll 运行该程序。
  进一步程序要求输入选项, 咱们输入1,就显示了结果:Hockeyhasthree 20 minute periods。]

3 如何用反射机制访问对象的类型信息

  好了,今天就到这里了,下面我将进一步说明如何用反射机制访问对象的类型信息
  我不想在这里过多的描述反射的概念。我还是用我自己觉得最简单、最直接的语言来描述反射——“反射就是一种机制,通过这种机制,能知道一些位知程序集的详细信息!”;通过上一篇已经学会如何得到一个未知程序集的相关信息, 接下来我要讲的是如何知道未知程序模块的信息:
  模块信息是通过 Module 类访问的。下面通过一个类子,讲解下 Module 类的使用,如果你是一个用心的程序员,应该了解下 Module 的详细信息。
  下面写一个新的文件 ModuleDemo.cs 。内容如下:

using System;
using System.Reflection;
public class ModuleDemo
{
    public static void Main(string[] args)
    {
        //=======================
        // Am Module object is obtained representing the
        // SomeSports.dll library file
        //=======================
        Assembly assembly = Assembly.Load("SomeSports");
        Module module = assembly.GetModule("SomeSports.dll");
        //======================
        //Search the module for the type named "Football"
        Type[] types = module.FindTypes(Module.FilterTypeName, "Football");
        if (types.Length != 0)
        {
            ConstructorInfo ci = types[0].GetConstructor(new Type[0]);
            Sport sport = (Sport)ci.Invoke(new Object[0]);
            Console.WriteLine(sport.GetName() + " has " + sport.GetDuration());
        }
        else
        {
            Console.WriteLine("type not found");
        }
    }
}

  用 csc /r:Sport.dll ModuleDemo.cs 编译,然后用 MouduleDemo 运行程序就能看到如下输出:Football has four 15 minute quarters。
  关于 C# 反射的基础知识,还有一个知识点就是访问未知对象的类型信息。

七 类的实例化过程

1.属性、方法不需要初始化,因为这些全部是指针。
2.初始化派生类的静态字段。
3.初始化派生类的非静态字段。
4.初始化基类的静态字段。
5.初始化基类的非静态字段。
6.调用基类的构造函数。
7.调用派生类的构造函数。

1.静态变量设置为0
2.执行静态变量初始化器
3.执行基类的静态构造函数
4.执行静态构造函数
5.实例变量设置为0
6.执行衯变量初始化器
7.执行基类中合适的实例构造函数
8.执行实例构造函数

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值