一、初始化器、构造器、析构器

1.0对象初始化器

class Student
{
    public String name;
    public int age { get; set; }
}
internal class Program
{
    static void Main(string[] args)

    {  //写法一
        Student stu=new Student();
        stu.name="Tom";
        stu.age=20;
       //写法二
        Student stu2 = new Student { name = "Tom", age = 20 };

在 C# 中, Student stu2 = new Student { name = "Tom", age = 20 }中的 {} 称为对象初始化器(Object Initializer),它是 C# 3.0 引入的语法糖,用于在创建对象时使用键对值方式直接初始化对象的公共属性或公共字段,简化代码书写,等同于写法一;

1.1构造器(构造函数)

特点:

与类同名,没有返回值

不自己定义系统会生成一个默认的构造器,就是将给成员属性赋该类型默认值

自己定义默认构造器会被覆盖,而不是重载(因为无法再调用系统自动生成的构造器)

1.1.1实例构造器和静态构造器

静态构造器不能有修饰限定符

静态构造器不能有参数

静态构造器不能重载

 class student
 {
     public int score { get; set; }
     public static int num { get; set; }
     //实例构造器
     public student(int score)
     {
         this.score = score;
         num++;
     }
     //静态构造器
      static student(){
         num = 100;
     }
 }

1.1.2实例构造器和静态构造器的调用时机

实例构造器
  1. 每次使用 new 关键字创建类的实例时,必然会调用实例构造器(如果有多个重载,会根据new 后的参数匹配对应的构造器)。
  2. 若类有继承关系,创建派生类实例时,会先调用基类的实例构造器,再调用派生类的实例构造器(确保基类的实例成员先初始化)
静态构造器

静态构造器是类被首次 “访问” 时自动调用的方法,用于初始化类的静态成员(静态字段、静态属性等),与实例无关

  1. 首次创建该类的实例(new实例时)。
  2. 首次访问该类的静态成员(静态字段、静态属性、静态方法)。
  3. 首次使用该类作为泛型类型参数(如 List<MyClass> 首次被使用时,若 MyClass 未初始化则触发其静态构造器
  4. 类有继承关系,基类的静态构造器先于派生类的静态构造器执行

1.2析构器(析构函数)

不加修饰限定符,使用(~类名)方式来定义函数

c#有资源回收器(GC),但是调用底层资源时有用

2、类的声明

c#类的声明和定义不分家

声明位置

  1. 把类声明在名称空间中
  2. 也可以把类声明在文件的名称空间外,但是c#有一个global名称空间,因此也在名称空间内部(不推荐)
  3. 可以声明在类中(成员类)

3、类的继承和类成员的访问控制

“是一个”规则:父类引用变量可以存储子类实例的地址,含义为:car是一个vehicle   

   static void Main(string[] args)
    {
        vehicle v = new car();
        object o = new vehicle();
        object o1 = new car();      
    }
 
}
 class vehicle
{

}
class car : vehicle
{

}
  • sealed关键字:封闭类,不能当作基类来使用,下例:编译器报错
  • 一个类只能继承一个基类
  • 一个类可以实现多个基接口
  • 子类的访问级别不能超过基类
  • 父类的构造器是不能被继承的

3.2编译器的调用规则

当创建子类实例时,父类的构造器先被启动,作用是初始化子类中继承的属性的值,下例中我们可以看到car中没有对owner进行赋值,但是任然可以输出值vehicle,而如果子类构造器对他赋值了,那么会覆盖这个结果。

    static void Main(string[] args)
    {
        car mycar = new car();
        Console.WriteLine(mycar.owner);   
    }
}
 class vehicle
{   
    public vehicle() { 
        this.owner = "vehilce";
    }
    public string owner { get; set; }

}
class car : vehicle
{
    public car()
    {    }
}

上例是父与子类构造函数无需参数情况,需要参数时需要使用base关键字,下例

 class vehicle
{   
    public vehicle(string own) { 
        this.owner = own;
    }
    public string owner { get; set; }

}
class car : vehicle
{
    public car(string own):base(own)
    {  
    }
}

3.3类的访问级别和类成员的关系

类成员的访问级别的上限是类的访问级别

类的默认访问级别是internal,成员的默认访问级别是private

protected:是可以跨程序集的,也就是说实际上,四种访问级别并不是严格的大小关系

3.4开闭原则

  • 对扩展开放:当需求变化时,可以通过添加新代码来扩展功能
  • 对修改关闭:不应该修改已有的、经过测试的代码

3.5重写、多态、隐藏

重写

下面代码中,父子类中同名方法,父类中方法使用virtual修饰,子类使用override修饰,这就是重写

多态

多态 = 继承 + 重写 + 父类引用指向子类对象

    static void Main(string[] args)
    {
        vehicle c = new car();
        c.run();
      //运行结果为:I am driving
    }
}
 class vehicle
{
    
    public virtual void run()
    {
        Console.WriteLine("I am running");
    }
}
class car : vehicle
{   
    public override void run()
    {
        Console.WriteLine("I am driving");
    }
}
class racecar : car
{
    public override void run()
    {
        Console.WriteLine("I am racing");
    }
}

隐藏

下面代码中没有使用,override和viritual修饰符,目前在子类中其实存在两个run方法,但是父类指针只能访问子类继承来的那一部分,也就是他自己的run方法,由代码可见同时存在两个run方法

    static void Main(string[] args)
    {
      vehicle v = new car();
      v.run();
      //输出结果为I an running
       car c =new car();
       c.run();
       //输出结果为I am driving
    }
}
 class vehicle
{
    
    public void run()
    {
        Console.WriteLine("I am running");
    }
}
class car : vehicle
{   
    public  void run()
    {
        Console.WriteLine("I am driving");
    }
}

3.5.1多态的原理

多态的形式是父类引用变量指向子类实例,其实他只能访问子类继承的部分,这也就解释,为什么隐藏时,输出结果为自己的run方法,而重写后,相当于继承的那个函数内容被替换了,同时其他的类成员,当没有重写的时候,仍然是父类的值。

3.5.2多态的作用

主要是对开闭原则的遵守

不使用多态

需要新增时:需要修改之前就写好的DrawAllShapes()修改,新增类

using System;

namespace SimpleExample
{
    // 三种形状
    public class Circle
    {
        public void Draw() => Console.WriteLine("画一个圆");
    }

    public class Square
    {
        public void Draw() => Console.WriteLine("画一个正方形");
    }

    public class Triangle
    {
        public void Draw() => Console.WriteLine("画一个三角形");
    }

    public class Program
    {
        // ❌ 问题:每次新增形状都要修改这个方法!
        public void DrawAllShapes()
        {
            var circle = new Circle();
            circle.Draw();  // 画一个圆
            
            var square = new Square();
            square.Draw();  // 画一个正方形
            
            var triangle = new Triangle();
            triangle.Draw();  // 画一个三角形
            
            // 如果要加一个Rectangle,必须在这里添加新代码!
            // 这违反了开闭原则
        }

        static void Main(string[] args)
        {
            var program = new Program();
            program.DrawAllShapes();
        }
    }
}
使用多态

新增类,在对象列表中修改,无需改动之前的函数

using System;
using System.Collections.Generic;

namespace SimpleExample
{
    // 定义统一接口
    public interface IShape
    {
        void Draw();
    }

    // 三种形状实现接口
    public class Circle : IShape
    {
        public void Draw() => Console.WriteLine("画一个圆");
    }

    public class Square : IShape
    {
        public void Draw() => Console.WriteLine("画一个正方形");
    }

    public class Triangle : IShape
    {
        public void Draw() => Console.WriteLine("画一个三角形");
    }

    // 新增形状:无需修改现有代码!
    public class Rectangle : IShape
    {
        public void Draw() => Console.WriteLine("画一个长方形");
    }

    public class Program
    {
        // ✅ 多态:统一处理所有形状
        public void DrawAllShapes(List<IShape> shapes)
        {
            foreach (var shape in shapes)
            {
                shape.Draw();  // 多态:自动调用对应形状的Draw方法
            }
        }

        static void Main(string[] args)
        {
            var program = new Program();
            
            // 创建形状列表
            var shapes = new List<IShape>
            {
                new Circle(),
                new Square(),
                new Triangle(),
                new Rectangle()  // 新增形状,无需修改DrawAllShapes方法!
            };
            
            program.DrawAllShapes(shapes);
        }
    }
}

3.5类的组合

在类的继承中,我们提到了”是一个“原则,即car是子类,vehicle是父类,那么car是一个vehicle,同时父类引用变量可以指向子类的实例;类的组合是”有一个“原则,一个类完全可以将另一个类作为自己的属性。

举个栗子:

 class people {
     public Address address { get; set; }
 }
 class Address { 
 public string city { get; set; }
 }
 
 internal class Program
 {
     static void Main(string[] args)
     {   //分别实例化两个类
         people p=new people();
         Address a = new Address { city = "New York" };
         p.address = a;
         Console.WriteLine(p.address.city);
         //简化写法
         people p2 = new people { address=new Address { city="AAA" } };
         Console.WriteLine(p2.address.city);

     }
 }

上面代码定义了两个类,people这个类直接使用Address作为他的属性,在我们给people这个类赋值的时候,有两种方式:

方式一:分别实例化两个类,将a这个Address类的引用变量赋值给p.address。

方式二:直接嵌套两个初始化器,在people的初始化器中给address创建一个实例,并且也使用初始化器。

二、抽象类和开闭原则

抽象类

定义:

使用abstract修饰的类叫做抽象类,里面使用abstract修饰的方法是抽象方法

特性:

抽象类不能被实例化

作用:

规定了子类必须实现抽象方法,可以约束子类行为

    实例:

     abstract class vehicle
    {
         public abstract void run();
    }
    class car : vehicle
    {   
        public override void run()
        {
            Console.WriteLine("car am driving");
        }
    }

    三、接口

    定义:

    用来规定一个class必须实现那些方法的合同

    特性:

    • 只定义方法的签名,不实现
    • 继承的类必须全部实现接口的方法
    • 一个类可以继承多个接口
    • 接口中函数不能写访问级别修饰符默认是public
    • 不能被实例化

    作用:

    可以约束class

    可以省略用abstract去修饰方法和类,也无需子类override

    实例:

        internal class Program
        {
            public interface Ivehicle
            {
                 void Run();
               
            }
            interface Ifireable
            {
                void Fire();
               
            }
           public class Tank : Ivehicle, Ifireable
            {
                public  void Run()
                {
                     Console.WriteLine("run");
                }
                public void Fire()
                {
                    Console.WriteLine("fire");
                }
            }
    static void Main(string[] args)
            { 
                Ifireable f = new Tank();
                Ivehicle v = new Tank();
                f.Fire();           
                v.Run();
                //下面的内容就会报错,道理等同于类继承的时候,父类引用变量只能访问自己有的那一部分
                //f.Run();
                //v.Fire();
            }
        }

    接口和类的区别

    接口只规定‘能做到什么”

    类规定“你是什么”且“能做到什么”

    举例:下面图片中,作为驾驶员我只需要拿IRunnable的引用变量作为实例的赋值对象,不关心你是SuperTank,还是HeavyTank

       interface Iphone
       {
           void Dail();
           void Pickup();
           void Send();
           void Receive();
       }
       class Nokia : Iphone
       {
           public void Dail()
           {
               Console.WriteLine("Nokia is dailing");
           }
    
           public void Pickup()
           {
               Console.WriteLine("Nokia is picking up");
           }
    
           public void Receive()
           {
               Console.WriteLine("Nokia is receiving");
           }
    
           public void Send()
           {
             Console.WriteLine("Nokia is sending");
           }
       }
    
       class userphone
       {
           private Iphone _phone;
           public userphone(Iphone phone)
           {
               _phone = phone;
           }
           public void Usephone() { 
           _phone.Dail();
           _phone.Pickup();
           _phone.Send();
           _phone.Receive();
           }
       }
       internal class Program
       {
           static void Main(string[] args)
           {
               userphone user = new userphone(new Nokia());
               
           }
       }

    3.2依赖反转

    由这张图解释上面代码

    不使用接口时:

    例如:有两个品牌的手机(Nokia和huawei)需要创建两个类(这一步都一样),手机的使用者(userphone)需要使用不同的手机(在这个类中调用手机类的方法)那么要不然就是创建两个(userphone)类似的类分别实例化,要不只能在一个类中写两个不同名的方法(不同点1),这时,手机和手机的使用者,是手机使用者依赖于手机,因为需要在手机使用者方法(Usephone)中实例化手机类才能调用手机中的方法(不同点2)对应图中左上角

    使用接口时:

    我们创建一个接口(Iphone),两个品牌的手机依赖于Iphone因为实现接口,使用者(userphone)依赖于(Iphone),这时依赖关系发生了变化,并且现在在使用者(userphone)中想要实现不同手机类中的方法,只需要使用接口类的引用类型,当实例时传入Nokia时调用Nokia的方法,传入huawei时调用huawei的方法

    使用接口的优势:

    当我们需要增加一个手机品牌例如三星,不使用接口时,除了写上三星类,我们需要修改手机使用者类,增加函数,或者直接专门写一个三星手机使用者的类,对应上图左上角

    使用接口时:只需要写上三星类,然后调用时把三星类的实例作为实际参数传给使用者类中的函数的接口的引用变量就可以了(对应上图右上角)

    在修改代码时不需要改变userphone类的成员,因为userphone不依赖于某个手机品牌的类,只依赖于接口,这样增加了类的独立性,我们称为解耦合

    更多推荐