Loading

0:c#教程-基础

2.1 基础

2.1.1 对象组合

对象组合:对象合作关系中的一种,其含义是“一个对象包容另一个对象”。
“一对一”对象组合的两种类型:

A对象完全包容B对象,生命周期被管理(一T对一、一对多)

//一对一 对象组合方式
Class OneToOneClass{
    private InnerClass obj;
    public OneToOneClass(){
        obj = new InnerClass();  // A对象内部创建B对象
    }
}

//一对多 对象组合方式(内部直接new,拥有相同的生命周期,如车的四个轮子)
Class OneToManyClass{
    private List<InnerClass> objs = new List<InnerClass>(); // 内部对象集合

    public void Add(InnerClass obj){ // 公有方法实现内部集合增删对象
        if(obj != null){
            objs.Add(obj);
        }
    }

    public void Remove(){ 
        //略
    }
}

B对象是独立的,A对象引用线程的B对象(一对一、一对多)

//一对一 对象组合方式
Class OneToOneClass2{
    private InnerClass obj = null;
    public OneToOneClass2(InnerClass outerObj){
        this.obj = outerObj;  // 包容的对象由外界负责创建,通常采用对象注入的方式
    }
}

//一对多 对象组合方式(对象注入,生命周期相互独立,如飞机场上的飞机)
class OneToManyClass2<T>{
    private IEnumerable<T> objs = null;

    public OneToManyClass2(IEnumerable<T> objCollections){
        objs = objCollections;
    }
}

// 一对多,对象注入
List<InnerClass> innerObjs = new List<InnerClass>();
innerObjs.Add(new InnerClass());
  //多创建几个后
OneToManyClass2<InnerClass> outer2 = new OneToManyClass<InnerClass>(innerObjs);

对象组合实例

如VS主界面包含诸多控件,就是采用窗体嵌入的方式,分为几块大的容器,然后每个容器可以动态加入新的控件进去。当程序启动时,先显示整个框架,然后在后台线程中构建具体的子界面,等到子界面创建好之后,再嵌入到窗体中。这是一种典型的使用延时动态组合的方式。

对象组合的特殊形式:自引用类

在没有指针的面向对象编程语言中,自引用类用于代替指针建立数据之间的关联。例如通过自引用类实现链表。

class MyClass{
    MyClass obj;
}

统一建模语言

巩固练习:对象组合

使用自引用类实现下图所示之二叉树,并且对此树进行深度前序遍历(即从根节点出发,先访问根节点,如果有左右节点,先访问左节点,再访问右节点,对每个节点重复此过程)。

待续;

2.1.2 对象复制

需要实现ICloneable接口的Clone方法,层层递进实现深复制。

class ClassA: ICloneable{
    public int valA = 100;
    public ClassB EmbedObj; // ClassA 包容一个ClassB的对象,那B对象也需要实现ICloneable接口
    
    //ICloneable的Clone方法
    public Object Clone(){
        ClassA objA = new ClassA();
        objA.valA = this.valA;  // 复制属性
        objA.EmbedObj = (this.EmbedObj as ICloneable).Clone as ClassB; //通过ClassB的Clone方法复制一个B对象成员
        return objA;
    }
}

2.1.3 对象序列化

对象序列化主要解决对象状态的保存问题。

对象状态,其实就是某一时刻对象拥有的各个字段值的集合

  • 对象状态保存到另外的一种媒介中,并在需要的时候可以从媒介中重新读取数据重建对象的过程称为对象的“序列化”与反序列化。
  • 在开发中,常用于保存对象状态的媒介:流(Stream) 和 字符串(String)

流是抽象的概念。代表一连串有顺序的二进制数据。文件流
image
image

几种流对象:
image

对象序列化方式:

  • 二进制序列化:将对象的数据看成是二进制的数据而直接写入流中。(格式化器:BinaryFormatter)
  • XML序列化:将对象数据用MXL方式表示之后再以纯文本的方式写入到流中。(格式化器:SoapFormatter)

待:实例:程序的状态保存;对象的保存;
大批的复制对象,深复制的便捷方式。
分布式系统的对象序列化

image

2.1.4 对象的比较

2.1.5 对象间的协作与信息交换(重要)

对象协作的本质就是对象间信息交换的问题,体现为对象之间的相互访问,即:

  • 相互存取字段/属性
  • 相互调用方法

直接通过类本身的字段和属性来完成

一对一,主->从 对象间信息传送

主窗体通过从窗体对象的引用,调用其公有方法/属性传送信息。

一对一,从->主 对象信息传送

主、从窗体例子中,实现方法:

  • 从窗体设置公有方法,主窗体依据从窗体提供的状态信息和返回值,进行处理显示
  • 从窗体“主动”向主窗体“汇报工作”,这种方法需要用到两个编程技巧:
    1. 对象注入:从窗体使用this把自己引用传给从窗体
    2. “回调”:从窗体回调主窗体的共有方法

回调:一般情况:new一个对象,然后调用它的方法。然而,如果我们(假设为A)new了一个对象B,告诉它在某条件下,可以调用XXX方法,之后不管;等B在合适的时候,自己调用A告诉它的方法,这就叫“回调”。简单来说,这个方法何时被调用不由A做主。

实践:两个对象间信息的双向传送
一对多,主=>从,对象间的“信息广播”
  • 使用对象集合的方法实现“消息广播”:主窗体使用一个对象集合保存所有已创建从窗体对象的引用;点击按钮时,遍历这个对象集合,逐个调用从窗体对象的公有方法显示信息。
  • 主窗体定义一个委托变量,在创建从窗体对象时,将从窗体的公有方法挂接到这个字段上;实现一次点击,同时更新显示
  • 主窗体定义一个MyClick事件,当点击按钮时,触发这个事件;从窗体的公有方法ShowCounter()方法响应这个事件,当事件触发时,刷新显示。
    利用委托实现“消息广播”,实现过程:
    主窗体定义一个ReceiverMethods委托变量,在创建从窗体对象时,将 从窗体 的共有方法 showCounter 挂接到这个变量上;
    主窗体点击按钮时,只需要调用一次ReceiverMethods,所有 从窗体都能更新显示。
    本质是主窗体“回调”从窗体的方法。
一对多,主<=从,一个对象“监控”多个对象
  • 利用对象引用回调:从窗体有一个字段引用主窗体对象,当从窗体点击按钮时,它通过对象引用调用主窗体的方法向其“主动汇报情况**
  • 利用委托回调:主窗体在创建从窗体对象时,把它自己的共有方法ShowCounter挂接到主窗体的委托对象上;当从窗体点击按钮时,通过委托回调主窗体的方法
  • 自定义事件:从窗体定义一个MyClick事件,主窗体的方法中响应这个事件(方法挂接到事件上),在事件响应代码中累加计数并更新显示。=>一个对象响应多个对象触发的事件。
  • 全局静态字段和方法,静态计数器,静态主窗体引用,静态方法,从窗体直接调用即可。不推荐
多对多 对象间的协作与信息交换

如果使用“对象之间相互只有对方引用”方式实现对象的通讯,太多太乱了。当添加和移除新对象时,维护成本过高。
解决方案:

  • 该“多对多”为“一对多”:添加一个中介者作为消息交换中心。

2.2 委托与事件

2.2.1 委托基础

委托是事件、异步调用、多线程开发的技术基础。

定义一个委托

public delegate int MathOptDelegate(int value1,int value2); // 可以接受一个 有两个int参数和int返回值的函数引用
MathOptDelegate oppDel; //定义一个委托变量
MathOpt obj = new MathOpt();  // 类 MathOpt,定义了Add方法(
oppDel = obj.Add;       //委托变量接受一个方法引用
Console.WriteLine(oppDel(1,2));  // 调用

多路委托,使用+=挂接方法,使用-=移除方法的引用; 组合委托

MyClass obj = new MyClass();
MyDelegate del1 = new MyDelegate(obj.Func1);
del1 += new MyDelegate(obj.Func2); // 挂接方法

Delegate[] ds = del1.GetInvovationList(); //获取方法调用列表<<<<

del1(5);  //堆,先进先出,先调用Func1,再调用Func2

//简写:
MyDelegate del2 = obj.Func1;
obj2 += obj.Func2;

//组合委托
MyDelegate mu1 = del1 + del2; //此时有包含四个方法引用
mu1(10); //之后返回委托调用列表最后一个方法的返回值
mu1 -= obj.Func2; // 移除最后一个方法引用
//从头开始调用方法,从尾开始查找移除方法的引用

匿名方法

public delegate int Del(int i,int j);

Del del = delegate(int i,int j){
    return i + j;
}
//或者
Del del = delegate((i,j) => i+j);

委托类型直接作为方法形参

public static void invokeDelegate(Del del,int i, int j){ //Del是定义的委托变量,del是一个方法
    Console.WriteLine(del(i,j));
}
//调用
invokeDelegate((i,j)=>i+j,100,200);

编程中如何应用

可以引用一个方法调用列表,并且可以随时改变它所引用的方法列表。
委托的本质特征是“一对多”,一个委托变量对应多个方法。

实例:定时回调

2.2.2 泛型委托,预定义委托

  • 泛型委托使用与普通委托一样,只是在定义的时候指定泛型参数即可。
  • 使用预定义委托,无需从头定义自己的委托类型。常见的两种: Func<> 和 Action<>
    • Func<>: 最后一个参数是委托所接收方法的返回值,前面参数就是委托所接收方法的形参。
    • Action<>: 接收返回void方法的预定义委托
    • 泛型委托=>泛型委托变量;语法糖:直接使用预定义委托来定义泛型委托变量。
//普通委托
public delegate int Del(int i,int j);
Del del =(i,j) => i+j;

//定义泛型委托
public delegate T Del<T>(T obj1,T obj2); //定义指定泛型参数
Del<int> del = (i,j)=>i+j;

//使用预定义委托 Func<>
Func<int,int,long> del = (i,j) => i+j; //i和j为int类型,并且返回long类型的值

2.2.3 委托与事件

事件三要素:

  • 事件源,激发事件的对象
  • 事件信息,事件本身携带的信息
  • 事件响应者,响应事件的代码,就是当事件发生时,计算机需要完成的工作

事件驱动

“事件驱动”的软件运行方式:事件触发时,响应事件的代码被调用。
例如:事件触发:鼠标移动;事件响应者:输出lblInfo.Text

private void Form1_MouseMove(object sender, MouseEventArgs e){
    lblInfo.Text = string.Format(
        "事件信息:({0},{1}) \n事件源自:{2}",
        e.X, e.Y, sender.GetType()
    );
}

“事件驱动”的软件开发方式为:

  1. 定义并实现自己的事件,或者从系统组件库中选择一种现成的事件;
  2. 为这事件编写响应代码

如何自定义事件?事件的特征?
主要特点为一对多关联,一个事件源可以有多个响应者。
=> 委托也具有一对多关联的特性。

利用委托实现自定义事件

//定义事件委托
public delegate void MyEventDelegate(int value);

//事件发布者类(事件源)
public class Publisher{
    //利用多路委托变脸保存多个事件响应者方法引用
    public MyEventDelegate MyEvent;
}

//事件响应者类
public class Subscriber{
    //事件触发时的回调方法
    public void MyMethod(int value){
        Console.WriteLine(value);
    }
}
//-----------------------------------------------------------------
// 模拟
//一个事件源对象
Publisher p = new Publisher();
//两个事件响应者
Subscriber s1 = new Subscriber();
Subscriber s2 = new Subscriber();

//挂接事件响应方法
p.MyEvent += s1.MyMethod();
p.MyEvent += s2.MyMethod();

//触发事件
p.MyEvent(10);

使用关键字event用于自定义事件

与使用多路委托的实现方法类似,不同之处在于其增添了一个新的特性:
事件只能有事件源对象自己激发,外界无法通过访问委托变量直接激发事件。

//事件发布者类
pubLic class Publisher{
    public event MyEventDelegate MyEvent; // 多了个event
    
    //激发事件的对象
    public void FireEvent(int EventArgu){ //外界无法直接触发MyEvent事件,所以只能通过共有方法间接触发
        if(MyEvent != null){
            MyEvent(EventArgu);
        }
    }
}

//一个事件源,两个事件响应者
//挂接事件响应方法
//都相同,只是不能直接触发: p.MyEvent() ,编译不通过
p.FireEvent(10);

代码阅读能力训练

阅读WinForm中Button的源代码,弄清楚它的Click事件是如何触发的。

.Net事件的触发与响应机制是建立在委托之上的。

套路?

让事件参数从EventArgs类派生,比如定义一个事件参数类MyClickEventArgs,从EventARgs类派生

实践:自定义按钮,统计点击次数。

  1. 添加自定义按钮 myclick,具体代码在:c_sharp_sample_event
  2. 使用泛型事件委托(更合适的做法)
    => 为了简化自定义事件开发,.NET基类库中自定义了一个通用的泛型事件委托。EventHandler
//继承 EventArgs类,添加一个属性ClickCount,在构造方法接收点击次数的值
//新建 MyClick 事件,事件参数是上面那个
//重写OnClick方法,在原有功能的基础上(base.OnClick(e)),添加MyClick的判断与响应
public partial class MyCustomButton : Button
{
    public MyCustomButton()
    {
        InitializeComponent();
    }

    private int ClickCount = 0;
    //实现方法1:先定义委托,再定义事件
    public delegate void MyClickEventDelegate(object sender,MyClickEventArgus eventArgs);
    public event MyClickEventDelegate MyClick; 
    //实现方法2:定义泛型事件委托
    //public delegate void EventHandler<TEventArgus>(Object sender, TEventArgus eventArgs) where TEventArgus:EventArgs;
    public EventHandler<MyClickEventArgus> MyClick2;
    protected override void OnClick(EventArgs e)
    {
        base.OnClick(e);
        ClickCount++;
        //实现方法1:
        MyClick?.Invoke(this, new MyClickEventArgus(ClickCount));
        //实现方法2:
        MyClick2?.Invoke(this, new MyClickEventArgus(ClickCount));

    }
}

public class MyClickEventArgus : EventArgs
{
    public readonly int ClickCount;
    public MyClickEventArgus(int ClickCountValue)
    {
        ClickCount = ClickCountValue;
    }
}

实践?不一定>一个方法响应多个控件的事件:
窗体上有三个按钮,点击不同按钮,标签控件显示出相应的信息:按钮XXX被单击。
要求:
只能使用一个事件响应方法,响应三个按钮的单击事件。
事件响应方法中需要区分出到底是哪个按钮被单击的,你是怎么做的?

未:对象序列化,程序信息的保存
实践:两个对象间信息的双向传送
对象间的信息广播,使用event实现点击广播信息

posted @ 2025-03-12 22:27  一起滚月球  阅读(13)  评论(0)    收藏  举报