基础知识总结2(c#)

接口、抽象类、类的概述及区别、使用场景

   1.类(class)

  • 类是面向对象编程的基本组成单元,用于描述属性和行为的对象集合
  • 类可以包含属性(也成字段或成员变量)和方法(也称函数或成员函数)
  • 类可以被实例化为对象,每个对象都有自己的状态(属性值)和行为(方法)
  • 类可以被继承,也可以继承其他类,但不能继承多个类(父类只能有一个)。

  2.抽象类(Abstract Class):

  • 抽象类是不能被实例化的类,他存在的意义在于作为其他类的基类,用于继承和重用
  • 抽象类可以包含抽象方法,这些放大只有声明而没有具体的实现
  • 子类必须实现抽象类中的所有抽象方法,否则子类也必须声明为抽象类

  3.接口(Interface):

  • 接口定义了一组方法、字段的契约,单不包含具体实现。
  • 类可以实现一个或多个接口,并提供接口中定义的方法的实现
  • 接口提供了一种方式来实现多态性,既一个对象可以被看作是他实现的任何接口类型

区别

  • 抽象类可以包含方法的实现、二接口只能包含方法的声明
  • 类可以实现多个接口,但只能继承一个抽象类
  • 抽象类可以包含多个抽象方法,接口不能包含非抽象方法,接口只能声明public的方法
  • 抽象类的目的是为了实现代码重用和提供一种约束,而接口的目的是为了实现多态性和实现类之间的松耦关系
  • 抽象类是接口与类的中介。

 使用场景

  • 当多个类具有共同的行为,但每个类的实现细节可能不同时,可以使用抽象类
  • 当需要定义一组类共同遵循的契约,但这些类可能来自不同的继承体系时,可以使用接口
  • 接口通常用于定义与特定领域无关的行为,而抽象类通常用于定义特定领域的通用行为
组合类和继承的优缺点及使用场景

  继承:

    优点:

  • 代码重用:之类可以继承父类的属性和方法,避免了重复编写相似的代码
  • 扩展性:通过扩展已有来创建新类,可以快速实现新功能

     缺点

  • 耦合度高:破坏封装,子类和父类存在紧密的关联,子类的修改可能会影响到父类和其他之类
  • 继承层次复杂:过深或过复杂的继承层次会增加代码的复杂性和理解难度
  • 局限性:只支持单继承

     适用场景:

  • 当子类是父类的一种特殊类型,并且可以继承父类的大部分属性和方法时,适合适用继承

   组合:

     优点:

  • 松耦合:组合将类之间的关系降低到一个松耦合的成都,一个类的变化不会对另一个类造成影响
  • 灵活性:通过组合,可以动态的在运行时改变对象之间的关系,更容易实现复杂的行为
  • 复用性:组合将已有的类组合创建新的类,对局部类进行包装、封装,提供新的接口,提高了代码的可重用性

      缺点:

  • 代码量增加:组合可能导致更多的类和对象,增加代码的复杂度
  • 设计难度:设计良好的组合关系需要考虑更多的因素,可能增加设计和实现的复杂性
  • 性能:创建组合类是,需要创建所有的局部类的对象,性能增加

      适应场景:

  • 当一个类需要使用另外一个类的功能,但不需要继承其所有的功能,适合使用组合
  • 当需要在运行时动态的改变对象之间的关系,或实现一种松耦合的设计时,可选择组合
析构函数是什么?,析构函数什么时候调用

     析构函数是一个特殊的方法,用于在对象被销毁时执行清理操作。方法名字和类名相同,但前面有一个波浪号(~)

     调用:

  • 当对象实例被销毁时,也就是当对象的生命周期结束时。这通常发生在程序结束时,或当对象所在的作用域结束时(例如,他是一个局部变量)
  • GC释放对象时,如果对象处于不在使用的状态,垃圾回收期可能会调用析构函数来进行额外的清理操作
  • 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

   注:c#的析构函数通常用于释放非托管资源,比如打开的文件,数据库连接或者其他外部资源,c#资源释放,推荐使用‘IDisposable’,因为这种方式更加可控,而且不依赖垃圾回收器的工作

 类怎么实现两个包含相同方法签名的接口

      当类需要实现两个包含相同方法签名的接口时,需要在类中显示的实现这个方法,已满与接口的要求

using System;

// 定义第一个接口
interface IInterface1
{
    void MyMethod();
}

// 定义第二个接口
interface IInterface2
{
    void MyMethod();
}

// 实现类同时继承两个接口
class MyClass : IInterface1, IInterface2
{
    // 显式实现接口的方法
    void IInterface1.MyMethod()
    {
        Console.WriteLine("Implementation of MyMethod from IInterface1");
    }

    // 显式实现接口的方法
    void IInterface2.MyMethod()
    {
        Console.WriteLine("Implementation of MyMethod from IInterface2");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 创建 MyClass 实例
        MyClass obj = new MyClass();

        // 通过接口调用 MyMethod
        ((IInterface1)obj).MyMethod(); // 调用来自 IInterface1 的实现
        ((IInterface2)obj).MyMethod(); // 调用来自 IInterface2 的实现
    }
}
什么是虚方法,他和抽象方法有什么不同

     虚方法:虚方法是指在基类中声明的方法,可以被子类继承并重写,在面向对象的继承关系中,通过虚方法可以实现多态性,既同一个方法名可以在不同的子类中表现出不同的行为。虚方法通过在方法声明前加上关键字‘virtual ’来定义。

     抽象方法:抽象方法时一种在基类中声明但不进行实现的方法,只包含方法签名而不包含方法体。抽象方法必须在子类中被重写才能给我具体的实现。抽象方法通过在方法声明前加上关键字‘abstract’来定义抽象,且包含抽象方法的类必须抽象类。抽象类本身不能被实例化,只能被继承。

   区别:

  • 虚方法可以在基类中有默认的实现,子类可以选择性覆盖。而抽象方法必须在子类进行具体的实现,否则子类也必须声明为抽象类
  • 虚方法通常用于允许子类修改或扩展父类的行为,而抽象方法通常用于定义一种接口,要求所有子类必须实现这个发方法。
方法重载和方法覆盖

    方法重载和覆盖都是一种多态性 

  • 方法重载是指我们有一个名称相同但签名不同的函数
  • 方法覆盖是当我们使用override 关键字覆盖子类中基类的虚方法或抽象方法时
static关键字

‘static’关键字声明静态成员或静态类 。使用 static 修饰符可声明属于类型本身而不是属于特定对象的静态成员。static 修饰符可用于声明 static 类。在类、接口和结构中,可以将 static 修饰符添加到字段、方法、属性、运算符、事件和构造函数。static 修饰符不能用于索引器或终结器。

静态成员:

在类中声明的静态成员是与类本身相关联的,而不是与类的实例关联。这意味着无需创建类的实例就可以访问静态成员,通过类名直接访问即可。

  • 静态字段:静态字段在整个应用程序中只有一个副本,所有类的实例共享同一个静态字段的值
  • 静态方法:静态方法不需要通过类实例化来调用,可以直接使用类名调用。可用于辅助方法,工具方法等
  • 静态属性:静态属性可用于获取或设置与类相关但不依赖与特定实例的值。

静态类:

静态类是一种特殊的;类,他只能包含静态成员,且不能被实例化。静态类在应用程序启动时被阿吉仔到内存中,并且其中的静态成员在整个应用程序生命周期内可用。

static使用场景:

  • 工具方法:如数学计算函数,字符串处理
  • 共享数据:如全局配置信息,计数器
  • 单例模式:
 readonly 关键字

readonly用于声明制度字段。只读字段是指其值只能在声明时或在构造中进行初始化,一单迟淑华完成,就不能修改。

使用场景:

  • 常量字段:如果你希望在类中定义一个常量字段,但又不想用‘const’关键字(‘const’只能用于编译是常量),可以使用‘readonly’
  • 线程安全:在多线程环境中,使用‘readonly’ 可以提高字段的线程安全性。因为‘readonly’字段在初始化后不可修改,所以不会出现多线程同时修改字段值的情况。注:对于值类型(如int,bool等)或不可变的引用类型(如string),readonly是绝对线程安全的。但是对于可变的引用类型(如list,dictionary或自定义类),虽然无法改变readonly字段本身引用的对象,但仍可以修改改对象的内部状态,可能遇到线程安全问题,如向list添加元素。
    public class MyClass
    {
        public readonly List<int> MyList = new List<int>();  // 可变引用类型
    
        // ...其他代码...
        
        public void AddItem(int item)
        {
            // 需要保证线程安全,因为MyList是可变的
            lock (MyList)
            {
                MyList.Add(item);
            }
        }
    }
    static 和readonly 联合使用的好处
  • 应用程序运行中保持唯一且不可修改的字段

virtual关键字

virtual 关键字用于修改方法、属性、索引器或事件声明,并使用他们可以在派生类中被重写(使用override关键字对虚方法重写)。示例:

 // 基类虚方法声明
   class BaseClass  
    {  
        public virtual void Method1()  
        {  
            Console.WriteLine("Base - Method1");  
        }  
  
        public virtual void Method2()  
        {  
            Console.WriteLine("Base - Method2");  
        }  
    }  
  
    class DerivedClass : BaseClass  
    {  
        // 重写基类中的虚方法
        public override void Method1()  
        {  
            Console.WriteLine("Derived - Method1");  
        }  
  
        public new void Method2()  
        {  
            Console.WriteLine("Derived - Method2");  
        }  
    }  

在这个示例中,DerivedClass 中的 Method2 使用了 new 关键字隐藏了 BaseClass 中的同名方法。Method1使用override 重写父类方法。

sizeof 关键字

sizeof是一个运算符而不是关键字。它用于计算特定类型或未知类型的大小(以字节为单位),通常用在编写低级代码时确定数据结构的大小。

sizeof 运算符的语法是:

sizeof(type)

 其中 type 可以是任何值类型、指针类型或未知类型。

注意事项:

  • sizeof  运算符只能用于值类型、指针类型和未知类型。他不能用于引用类型。
  • sizeof  运算符在编译时求值,而不是运行时,因此,他只能用于静态常量表达式。
  • sizeof  运算符返回的类型的大小,以字节为单位。

示例:

using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Size of int: " + sizeof(int)); // 输出:4(在大多数系统上)
        Console.WriteLine("Size of char: " + sizeof(char)); // 输出:2(在大多数系统上)
        Console.WriteLine("Size of double: " + sizeof(double)); // 输出:8(在大多数系统上)

        // 通过指针类型计算大小
        Console.WriteLine("Size of int* (pointer): " + sizeof(int*)); // 输出:8(在64位系统上)
    }
}
 in 关键字

in 关键字用于参数传递时,将参数按只读引用传递。他高速编译器在方法调用过程中不会修改改参数的值,并且可以通过引用传递避免对参数进行复制。对于大型结构或对象参数非常有用,因为直接引用参数可以提高性能的内存效率。

示例:

class Program
{
    static void Main(string[] args)
    {
        int x = 5;
        MultiplyByTwo(in x);
        Console.WriteLine(x); // 输出 5
    }

    static void MultiplyByTwo(in int number)
    {
        // 无法修改 in 参数的值
        // number *= 2; // 编译错误

        // 仅能读取 in 参数的值
        Console.WriteLine(number * 2); // 输出 10
    }
}

注:in 可用于协变的定义 

ref关键字
  • 参数在使用ref关键字进行引用传递时,必须在方法调用之前对其初始化。
  • 既可以在进入方法之前初始化参数的值,也可以在方法内部对参数进行修改。 
  • 如果方法需要对原始值类型变量进行修改,就需要使用ref关键来修饰对应的参数
            public void testMethod()
            {
                int aa = 1;
                Method1(aa);
                Console.WriteLine($"aa: {aa}");//输出 aa:1
                Method2(ref aa);
                Console.WriteLine($"aa: {aa}");//输出 aa:5
            }
    
            public  void Method1(int a)
            {
                a = 2;
                Console.WriteLine("Derived - Method1");
            }
    
            public void Method2(ref int a)
            {
                a = 5;
                Console.WriteLine("Derived - Method2");
            }

out 关键字 
  • 参数在使用out关键字进行引用传递时,不需要在方法调用前进行初始化。
  • out通常用于表示方法返回多个值的情况。
  • out参数必须在方法内部进行初始化,并确保在方法结束前完成复制这操作。方法内部没有为out参数赋值的情况下,方法调用将会导致编译错误。
不能将 in、ref 和 out 关键字用于以下几种方法
  • 异步方法,通过使用async 修饰符定义。
  • 迭代器方法,包括 yield return 或 yield break 语句。
  • 扩展方法的第一个参数,其中该参数是泛型类型(即使该类型被约束为结构。)
as和is的区别 
  • is 只是做类型兼容判断,并不执行真正的类型转换。返回true或false,不会返回null,对象为null也会返回false。

  • as运算符将表达式结果显式转换为给定的引用类型或可以为null值的类型。如果无法进行转换,则as运算符返回 null。

总结: as模式的效率要比is模式的高,因为借助is进行类型转换的化,需要执行两次类型兼容检查。而as只需要做一次类型兼容,一次null检查,null检查要比类型兼容检查快。

new关键字的作用
  • 运算符:创建类型的新实例

  • 修饰符:可以显式隐藏从基类继承的成员。

  • 泛型约束:泛型约束定义,约束可使用的泛型类型。

volatile  关键字

volatile 关键字用于声明字段,以指示编译器应该每次访问字段是从内存中读取其最新值,而不是使用缓存的值。他确保了字段的可见性,但不提供原子性或互斥性。

示例:

public class Worker
{
    private bool _shouldStop;

    public void DoWork()
    {
        bool work = false;
        // 注意:这里会被编译器优化为 while(true)
        while (!_shouldStop)
        {
            work = !work; // do sth.
        }
        Console.WriteLine("工作线程:正在终止...");
    }

    public void RequestStop()
    {
        _shouldStop = true;
    }
}

public class Program
{
    public static void Main()
    {
        var worker = new Worker();

        Console.WriteLine("主线程:启动工作线程...");
        var workerTask = Task.Run(worker.DoWork);

        // 等待 500 毫秒以确保工作线程已在执行
        Thread.Sleep(500);

        Console.WriteLine("主线程:请求终止工作线程...");
        worker.RequestStop();

        // 待待工作线程执行结束
        workerTask.Wait();
        //workerThread.Join();

        Console.WriteLine("主线程:工作线程已终止");
    }
}

在这个例子中,while (!_shouldStop) 会被编译器优化为 while(true)。我们可以看一下实际的运行效果来验证这一点。切换 Release 模式,按 Ctrl + F5 运行程序,运行效果始终如下:

程序运行后,虽然主线程在 500 毫秒后执行 RequestStop() 方法修改了 _shouldStop 的值,但工作线程始终都获取不到 _shouldStop 最新的值,也就永远都不会终止 while 循环。

我们修改一下程序,对 _shouldStop 字段加上 volatile 关键字:

public class Worker
{
    private volatile bool _shouldStop;

    public void DoWork()
    {
        bool work = false;
        // 获取的是最新的 _shouldStop 值
        while (!_shouldStop)
        {
            work = !work; // do sth.
        }
        Console.WriteLine("工作线程:正在终止...");
    }

    // ...(略)
}

此时在主线程调用 RequestStop() 方法后,工作线程便立即终止了,运行效果如下图所示:

这说明加了 volatile 关键字后,程序可以实时读取到字段的最新值。

注意,一定要切换为 Release 模式运行才能看到 volatile 发挥的作用,Debug 模式下即使添加了 volatile 关键字,编译器也是不会执行优化的。

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值