Loading

0:c#教程-概述

查看和调试 .net 源码
ildsm 工具查看程序集,SDK 目录下
ilspy 反汇编工具

1.3 C#面向对象编程基础

如何表达信息 --> 选择合适的数据结构
如何处理信息 --> 算法和程序控制结构
如何实现 --> 软件系统架构,面向对象的分析与设计

怎样构造求解问题的算法?

算法,主要指为了解决某个问题而设计的一种解决方案。
数据结构,对数据进行抽象
程序 = 数据结构 + 算法

面向过程编程,"从下层到上层"地逐层开发,先开发底层(被别人调用,自己不调用别人的函数),再开发中间层函数(调用底层已经实现好的函数),最后开发顶层函数(调用中间层的函数,注意最好不要跨层调用)。

面向对象编程,职责明确,封装。
好处:使用者简单易用,必要时可以修改算法。

类和对象

类的构建主要靠抽象。"抽象"的具体实施方法:提取出事物的本质,忽略所有不相关、不重要的信息,以"类"的形式表达出来。
观察的角度不同,目的不同,对同一事物进行抽象,会得到不同的抽象模型。例如人在公司中,可以抽象员工与经理;在学校中,则可以抽象出学生与老师。

类的定义与使用

  1. 使用new关键字创建“类的实例”,即对象。
  2. 用“类”类型的变量(即“对象变量”)来保存对创建出来对象的引用。
  3. 通过对象变量来访问对象的公有成员(包括方法和字段)。

对象与对象变量

变量的两种类型:引用类型和值类型。

  • “类”类型的变量属于“引用类型(Reference Type)”;其引用的对象占用的内存位于“托管堆(managed heap)”中。托管堆中的变量需要等到进程结束后,操作系统才会把整个内存进行回收。
  • int之类简单类型的变量属于“值类型(Value Type)”,值类型的变量占用的内存位于“线程堆栈(thread stach)”中。线程堆栈中的变量在线程结束后会自动销毁,其所占的内存就会被回收。

问题

  1. obj1 和 obj2 是否引用两个不同的变量? 答:引用的是相同地变量,赋的值是对象保存在托管堆中的地址。
A obj1 = new A();
A obj2 = null;
obj2 = obj1;
图片1
  1. Equals 和 ==
    在值类型的变量比较中,没有任何区别。在引用类型的变量中,Equals 会默认比较引用的类型的对象是否是同一个,不是则返回false;不过,可以为对象重写EqualsGetHashCode方法进行比较(字符串 string 是引用类型,就是重写了Equals 方法)

  2. this 是什么
    this是特殊的对象引用,代表对象本身。位于同一类内部的成员彼此访问,本质上是通过 this 这个特殊引用进行访问的,只不过这个关键字通常被省略了。
    所以,通过对象变量来访问对象的实例成员,是面向对象编程的一个基本准则(对象内部也通过 this 来访问)。

  3. 可以把值类型变量赋给引用类型吗? 答:可以,因为 C# 中最底层的基类都是 object。

int num = 123;
object obj = num; //装箱:值类型变量赋给引用类型变量 
int val = (int)obj; //拆箱:引用类型变量赋给值类型变量
// 装箱和拆箱会带来大的性能损失,需要尽量避免。

子类对象和父类变量间的矛盾和冲突

父类对象可以引用子类变量

Animal an = null;
Lion lion = new Lion();
an = lion; // 正确
lion = an;  // "编译"时出错
lion = (Lion)an; //正确,使用类型强制转换
Monkey monkey = (Monkey)an;  //"运行"时出错

方法同名:重载或重写

重载(Overload)参数类型、个数、次序不一致,重写(Override)则完全一致。

略

Parent p = new Child(); // 父类变量引用子类对象
p.OverloadF();   // 调用父类对象方法
(p as Chile).OverloadF(100)  //调用的是子类对象「重写」的方法
p.OverrideF();  //!调用的是子类对象「重载」的方法,, 需要在基类中定义虚方法(virtual)

扩展

new作为修饰符,override和new的区别

new 关键字可以显式隐藏从基类继承的成员,不加就是会警告。
使用new关键字:声明类是哪个类,就调用哪个类的方法;
使用virtual + override关键字:从实例类往基类方向找有没有实现此方法的类,找到的第一个执行。

字段同名:相互隐藏

class Parent{
  public int val = 100;
}
class Child:Parent{
  public int val = 200;
}

Parent p = new Child(); //父类变量引用子类对象
Console.WriteLine(p.val);  // 输出 100,即父类字段的值
Console.WriteLine((p as Child).val); //输出 200

我们可以看到,方法同名和字段同名最大的不同,在于重载时,父类调用的是子类的方法;而字段同名时,父类还是需要类型转换才能输出子类的字段值。所以,建议不要在子类中定义与父类同名的字段。

接口与回调

为什么会有接口?

鸭子是一种鸟,会游泳,也是一种食物,我们可以把鸭子作为 鸟 和 食物 的子类。但是会存在以下几个问题:

  1. C# 和 java 不支持多继承;
  2. “游泳”这个方法应该放在哪个类中?只有一部分鸟类会游泳。
    所以,我们可以将“鸟”作为“鸭子”的基类,“会游泳”和“食物”这些特性作为接口来实现相关方法。

接口的实际应用:回调

实现点击任意键,回调函数。代码在这里

image
// 主线程
Controler controler  = new Controler(); // 创建控制器对象
controler.AddCallback(new CallBackClass()); // 传入回调对象1,实现输出时间
controler.AddCallback(new CallBackClass2());  // 传入回调对象2,实现输出次数
// controler.AddCallback(new CallBackClass999())  // 可以在不更改原有代码的情况下扩展
controler.Begin(); //启动控制器对象运行

使用回调的好处在于可扩展性,如果需要在添加控制器运行的事件,不用更改原来的代码,只需要添加一个继承了ICallBack接口的CallBackClass类,实现相关方法即可。

多态

概念:同一操作用于不同的类的实例,不同的类将进行不同的解释,最后产生不同的结果。

多态的实现方式:继承多态接口多态

继承多态

通过抽象类和抽象方法实现的。
注意,抽象方法一定在抽象类中,抽象类不一定是抽象方法。

// C#中类或方法前加一个 `abstract`关键字就可以定义抽象类和抽象方法了。
abstract class Fruit(){
  public abstract void GrowInArea();
}

class Apple:Fruit{
  // 重载方法
  public override void GrowInArea(){
    // 一些话
  }
}

class Pineapple:Fruit{
  // 同上
  public override void GrowInArea(){
    //
  }
}

// 使用了‘多态’特性的方法,其代码具有稳定性,与具体子类无关
static void PolymorphismMethod(Fruit fruit){
  fruit.GrowInArea();
}

// 运行代码
//抽象类不能通过 `new` 直接创建对象,只能作为用它作为对象变量来引用子类的对象。
PolymorphismMethod(new Apple());
PolymorphismMethod(new Pineapple());

多态:代码有更强的适用性,多态特性可以帮助我们将需要改动的地方减少到最低限度。

对象的创建

对象注入:生命周期完全独立

一般来说直接使用new关键字创建实例,但还有一种对象注入的构造方式进行对象的创建。

class B{}

class A{
  // 私有变量用来保存类B的对象变量
  private B innerObj;
  
  public A(B obj){
    innerObj = obj;  // 用私有变量来保存外部注入的B对象的引用
  }
}

基于接口的对象注入

interface MyInterface{}

class B:interface{}   类B 实现了该接口

class A{
  private MyInterface innerObj;

  public A(MyInterface obj){
    innerObj = obj;  // 与上一个例子的区别在于,使用 MyInterface 作为对象变量的类型
    // 断开类A 和 类B 的强耦合性
  }
}

// 使用时
A obj = new A(new B);

基于抽象基类的对象注入

与基于接口类似

abstract class MyAbstractClass{};

// ... 后面都差不多

单例模式

只允许创建一个实例。

calss OnlyYou{
  private static OnlyYou obj = null; //或者这儿: obj = new OnlyYou();  // 底下直接 retunr obj;

  private OnlyYou(){  // 私有构造方法,使外界不能使用new直接创建对象·
  }

  public static OnlyYou GetOnlyYou(){
    if(obj == null){
      obj = new OnlyYou();
    }
    return obj;
  }
}

但在多线程环境下,可能会出现 OnlyYou被构造多次,比如线程一在创建过程中,线程二判断对象未创建,实际则是线程一未创建完成,所以线程二也会创建。
解决方案:

  1. 加锁 lock
public static OnlyYou GetOnlyYou(){
  lock(typeof(OnlyYou)){
    // 同上
  }
}

2.使用CLR支持的同步特性,效果与加锁类似,只不过是锁定在方法级别而不是锁定在类型级别

3.方法1和2都会造成性能损失,最好时使用类的静态构造函数,因为.net 虚拟机(CLR)会保证这个方法只被执行一次。

calss OnlyYou{
  private static OnlyYou obj = null;

  private OnlyYou(){ 
  }
  
  static OnlyYou(){ // 静态构造方法
    obj = new OnlyYou();
  }

  public static OnlyYou GetOnlyYou(){
    return obj;
  }
}

单例模式的巩固练习

在许多桌面应用中,都拥有超过一个以上的窗体,而不少应用都支持F1键调出帮助文档。对于显示帮助文档的窗体而言,当然你可以让用户每次压F1键时都创建一个新的窗体对象显示相应的内容,但更合适的方式是整个应用只实例化一个帮助文档显示窗体对象。这是一个比较适合于应用Singleton设计模式的场景。
请编写一个示例应用,应用Singleton设计模式实现某桌面应用的帮助文档显示功能。
要求:

  1. 整个应用只有一个帮助窗体对象
  2. 在不同的窗体压F1键,帮助文档窗体中应该显示不同的内容。

代码在这里,有个问题,help 关闭时,资源直接释放掉了。没成功,以后再说吧。

世上无难事,只要肯放弃。

对象销毁

C# 采用“隐式回收”的方式销毁对象,成为“垃圾回收(Garbage Collection)”。
析构方法:

  • 由于无法显式的重写Finalize方法,只能通过析构函数语法形式来实现
  • 执行垃圾回收之前系统会自动执行Finalize操作
  • Finalize方法会极大地损伤性能
  • 不建议重写析构方法**,应该让CLR负责销毁对象;如果需要显式回收资源,可以使用Disposable编程模式来解决。
class MyClas:IDisposable{
  ~MyClass(){
    Dispose(); // 析构函数中调用Dispose方法
  }

  public void Dispose(){
    // 编写代码释放资源 
  }
}
  • 析构方法通常用于清理非托管资源
  • Dispose() 方法,则可以同时清理托管和非托管资源
  • Dispose() 方法应该能被安全的调用多次
    IDisposable 编程框架
class MyClass:IDisposable{
  ~MyClass(){  Dispose(false); } // 只清理非托管资源
  public void Dispose(){
    Dispose(true); // 清理所有资源
    // 不需要再调用本对象的Finallize() 方法
    GC.SupperssFinalize(this);
  }
  protected virtual void Dispose(bool disposing){
    if(disposing){
      //清理托管资源
    }
    //q清理非托管资源
  }
}

using关键字

using(MyClass obj = new MyClass()){
  // do something...
}
// 相当于
MyClass obj = new MyClass();
try{
  // do something...
}finally{
  // 调用Dispose方法
  //所以使用using关键字的对象必须实现 IDisposable 接口
  IDisposable disposable = obj as IDisposable;
  if(obj != null){
    obj.Dispose(); // 释放资源
  }
}

托管资源与非托管资源

  • 托管资源:托管堆上分配的内存资源, 其由.NET运行时在适当的时候调用垃圾回收器进行回收(也可使用GC.Collect() 自主触发垃圾回收机制)。
  • 非托管资源:.NET不知道如何回收的资源,一般是与操作系统核心相关的资源,比如文件,数据库连接,网络连接、图标等。任何包含非托管资源的类,都应该继承IDisposable接口并实现Dispose()方法方法,以确保非托管资源的正确释放。

托管执行过程

  1. 选择编译器:即选择对应语言的编译器,来进行语言级别的处理,如语法检查;
  2. 将代码编译为 MSIL:将源代码转换为一组独立于CPU且可转换为本地代码的指令,即MSIL。
  3. 将 MSIL 编译为本机代码:.NET framework提供了两种转换方式:JIT(实时编译器)和NGen.exe(本机映像生成器)
  4. 运行代码
posted @ 2025-03-12 22:28  一起滚月球  阅读(24)  评论(0)    收藏  举报