菜鸟的C#学习(二)

发布于:2025-07-16 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、类的访问

在这里插入图片描述

1、普通类继承抽象类

在C#里,普通类继承抽象类时,有以下这些要点需要留意:

1. 必须实现所有抽象成员
抽象类中的抽象方法和属性不具备实现代码,继承它的普通类得把这些抽象成员全部实现出来。实现时,方法签名要和抽象类中定义的保持一致,并且要用override关键字。

public abstract class Shape
{
    public abstract double Area(); // 抽象方法
}

public class Circle : Shape
{
    public double Radius { get; set; }

    // 实现抽象方法
    public override double Area() => Math.PI * Radius * Radius;
}

2. 遵循访问修饰符的限制
在实现抽象成员时,访问修饰符要和抽象类中定义的一样。比如,抽象类里的抽象方法是protected,那么派生类中实现该方法时也得用protected

3. 不能直接实例化抽象类
抽象类没办法直接创建实例,必须通过派生类来实例化。

Shape shape = new Circle { Radius = 5 }; // 正确
Shape shape = new Shape(); // 错误,无法实例化抽象类

4. 可以添加新成员
继承抽象类的普通类能够新增自己的字段、属性、方法或者事件。

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double Area() => Width * Height;

    // 新增方法
    public double Perimeter() => 2 * (Width + Height);
}

5. 抽象类可以包含非抽象成员
抽象类中除了抽象成员,还能有已经实现的方法、属性等,派生类可以直接继承或者重写这些非抽象成员。

public abstract class Animal
{
    public string Name { get; set; }

    public void Eat() => Console.WriteLine($"{Name} is eating."); // 非抽象方法

    public abstract void MakeSound(); // 抽象方法
}

public class Dog : Animal
{
    public override void MakeSound() => Console.WriteLine("Woof!");
}

6. 抽象类也能继承自其他类或抽象类
如果抽象类继承了另一个抽象类,它可以选择实现部分抽象成员,剩下的由派生类去实现。

public abstract class Vehicle
{
    public abstract void Start();
}

public abstract class Car : Vehicle
{
    public override void Start() => Console.WriteLine("Car started."); // 实现基类的抽象方法
    public abstract void Drive(); // 定义新的抽象方法
}

public class SportsCar : Car
{
    public override void Drive() => Console.WriteLine("Sports car is driving fast.");
}

7. 不能用 sealed 修饰派生类
因为普通类要实现抽象类的抽象成员,所以不能用sealed关键字修饰该普通类,不然就没办法被其他类继承了。

总结
普通类继承抽象类时,要实现所有抽象成员,遵循访问修饰符的规则,不能实例化抽象类,不过可以添加新成员。抽象类可以有非抽象成员,还能继承其他类或抽象类。

2、普通类继承抽象类,抽象类继承接口,三者联系

当一个类(派生类)继承抽象基类,而抽象基类又实现了接口时,三者的成员函数关系遵循以下规则(以C#为例):

1. 接口定义“契约”,抽象基类部分或全部实现,派生类完成剩余实现

  • 接口:定义必须实现的成员(如方法、属性),但不提供实现。
  • 抽象基类
    • 必须“声明”实现接口的所有成员(即使只实现部分)。
    • 可将部分接口成员标记为 abstract(延迟到派生类实现),其他成员提供具体实现。
  • 派生类
    • 必须实现抽象基类中标记为 abstract 的接口成员(若有)。
    • 可选择重写(override)抽象基类中已实现的接口成员(若为 virtual)。

2. 示例说明
假设存在以下结构:

// 接口定义
public interface IMyInterface
{
    void MethodA();  // 接口方法
    void MethodB();
}

// 抽象基类实现接口
public abstract class MyAbstractBase : IMyInterface
{
    public void MethodA()  // 具体实现接口方法
    {
        Console.WriteLine("Base.MethodA");
    }

    public abstract void MethodB();  // 抽象方法,延迟到派生类实现
}

// 派生类继承抽象基类
public class MyDerivedClass : MyAbstractBase
{
    public override void MethodB()  // 实现抽象基类的抽象方法
    {
        Console.WriteLine("Derived.MethodB");
    }
}

成员关系分析

  • 接口 IMyInterface:定义 MethodA()MethodB()
  • 抽象基类 MyAbstractBase
    • 实现 MethodA(),派生类可直接使用。
    • MethodB() 标记为 abstract,强制派生类实现。
  • 派生类 MyDerivedClass
    • 无需关心 MethodA()(已由基类实现)。
    • 必须实现 MethodB(),否则会编译错误。

3. 特殊情况:抽象基类未完全实现接口
若抽象基类未实现接口的所有成员(即部分接口成员未被标记为 abstract 且未提供实现),则会导致编译错误。例如:

public abstract class MyAbstractBase : IMyInterface
{
    // 错误:未实现 MethodB(),且未声明为 abstract
    public void MethodA() { }
}

修正方式

  • MethodB() 声明为 abstract(如示例所示)。
  • 或在抽象基类中提供 MethodB() 的具体实现。

4. 接口显式实现与隐式实现
抽象基类可选择显式实现接口(只能通过接口类型调用):

public abstract class MyAbstractBase : IMyInterface
{
    void IMyInterface.MethodA()  // 显式实现接口方法
    {
        Console.WriteLine("Explicit implementation");
    }

    public abstract void MethodB();
}

此时,派生类需通过接口类型调用 MethodA()

MyDerivedClass derived = new MyDerivedClass();
((IMyInterface)derived).MethodA();  // 必须转型为接口类型

5. 派生类重写基类的实现
若抽象基类的方法为 virtual,派生类可选择重写:

public abstract class MyAbstractBase : IMyInterface
{
    public virtual void MethodA() { }  // 虚拟方法
    public abstract void MethodB();
}

public class MyDerivedClass : MyAbstractBase
{
    public override void MethodA() { }  // 重写基类方法
    public override void MethodB() { }  // 实现抽象方法
}

6. 多层继承的扩展
若存在多层继承(如抽象基类继承自另一个抽象基类),规则相同:

  • 每个抽象基类可实现部分接口成员,剩余抽象成员由最终派生类实现。
  • 示例:
    public interface IMyInterface { void MethodA(); }
    public abstract class Base1 : IMyInterface { public abstract void MethodA(); }
    public abstract class Base2 : Base1 { }  // 未实现 MethodA(),仍为抽象类
    public class Derived : Base2 { public override void MethodA() { } }  // 最终实现
    

总结

角色 对接口成员的责任 对抽象成员的责任
接口 定义所有成员签名
抽象基类 必须声明实现所有接口成员(部分或全部实现) 可定义抽象成员,强制派生类实现
派生类 实现抽象基类中未实现的接口成员(即抽象成员) 必须实现基类的所有抽象成员

这种分层设计允许:

  • 接口 定义统一契约。
  • 抽象基类 复用通用逻辑,简化派生类实现。
  • 派生类 专注于核心差异化逻辑。

二、类中方法的访问

在这里插入图片描述

2.1 抽象方法和虚方法

在C#中,抽象方法虚方法都用于实现多态性,但它们的设计目的和使用方式有本质区别。以下是两者的核心差异:

1. 定义语法与强制实现

抽象方法 虚方法
使用 abstract 关键字声明,且不能有方法体
csharp<br>public abstract void Print();<br>
使用 virtual 关键字声明,必须有默认实现
csharp<br>public virtual void Print() { Console.WriteLine("Base"); }<br>
必须由派生类实现,否则派生类必须声明为抽象类。 派生类可以选择是否重写,不重写时将继承基类的默认实现。

2. 所在类的限制

  • 抽象方法:只能存在于抽象类中(即使用 abstract 修饰的类)。
  • 虚方法:可以存在于普通类抽象类中。

3. 重写要求

抽象方法 虚方法
派生类必须使用 override 关键字实现,且不能使用 newsealed 隐藏基类方法 派生类使用 override 关键字重写(推荐),或使用 new 关键字隐藏基类方法(不推荐)。
示例:
csharp<br>public override void Print() { ... }<br>
示例:
csharp<br>public override void Print() { ... } // 重写<br>public new void Print() { ... } // 隐藏(不推荐)<br>

4. 设计目的

  • 抽象方法:用于定义必须由子类实现的契约,基类只规定方法签名,不提供默认行为。例如:
    public abstract class Shape
    {
        public abstract double Area(); // 所有形状必须计算面积
    }
    
  • 虚方法:用于提供可扩展的默认行为,允许子类在需要时修改实现。例如:
    public class Animal
    {
        public virtual void Speak() { Console.WriteLine("Animal sound"); }
    }
    
    public class Dog : Animal
    {
        public override void Speak() { Console.WriteLine("Woof"); } // 可选重写
    }
    

5. 调用方式

  • 抽象方法:无法直接调用,必须通过派生类的实现调用。
  • 虚方法:可以直接通过基类调用默认实现,也可以通过派生类调用重写后的实现。

总结对比表

特性 抽象方法 虚方法
方法体 不能有方法体 必须有默认实现
所在类 必须在抽象类中 可以在普通类或抽象类中
强制实现 派生类必须实现 派生类可选重写
关键字 abstract + override virtual + override(可选)
设计意图 定义必须实现的契约 提供可扩展的默认行为

示例代码

// 抽象类 + 抽象方法
public abstract class Vehicle
{
    public abstract void Start(); // 必须由子类实现
}

// 普通类 + 虚方法
public class Car : Vehicle
{
    public override void Start() { Console.WriteLine("Car started"); } // 实现抽象方法
    
    public virtual void Drive() { Console.WriteLine("Driving normally"); } // 虚方法,提供默认行为
}

// 派生类重写虚方法
public class SportsCar : Car
{
    public override void Drive() { Console.WriteLine("Driving fast!"); } // 重写虚方法
}

何时使用?

  • 使用抽象方法:当基类无法提供有意义的默认实现,且所有子类必须强制实现某个行为时。
  • 使用虚方法:当基类可以提供默认行为,但子类可能需要自定义实现时。

通过合理使用抽象方法和虚方法,可以构建出灵活且易于扩展的面向对象系统。

2.2 虚方法和普通方法

在C#中,虚方法virtual)和普通方法(无修饰符)的核心区别在于是否支持运行时多态。以下是两者的详细对比:

1. 调用机制

虚方法 普通方法
使用 virtual 关键字声明,支持运行时多态。基类的虚方法可以在派生类中被override重写。调用时,会根据对象的实际类型决定执行哪个版本的方法。 没有特殊修饰符,不支持运行时多态。调用时,根据对象的声明类型决定执行的方法,无论对象的实际类型是什么。
示例
csharp<br>public class Animal {<br> public virtual void Speak() { Console.WriteLine("Animal"); }<br>}<br><br>public class Dog : Animal {<br> public override void Speak() { Console.WriteLine("Dog"); }<br>}<br><br>// 输出:Dog<br>Animal animal = new Dog();<br>animal.Speak(); // 调用Dog的实现<br>
示例
csharp<br>public class Animal {<br> public void Speak() { Console.WriteLine("Animal"); }<br>}<br><br>public class Dog : Animal {<br> public new void Speak() { Console.WriteLine("Dog"); } // 使用new隐藏基类方法(不推荐)<br>}<br><br>// 输出:Animal<br>Animal animal = new Dog();<br>animal.Speak(); // 调用Animal的实现<br>

2. 方法重写

虚方法 普通方法
可以被派生类使用 override 关键字重写,从而改变方法的行为。 不能被重写,但可以使用 new 关键字隐藏基类方法(但这不是真正的重写,只是创建了一个同名的新方法)。
正确做法
csharp<br>public class Base {<br> public virtual void Print() { ... }<br>}<br><br>public class Derived : Base {<br> public override void Print() { ... } // 重写虚方法<br>}<br>
错误做法(隐藏而非重写):
csharp<br>public class Base {<br> public void Print() { ... }<br>}<br><br>public class Derived : Base {<br> public new void Print() { ... } // 隐藏基类方法(编译警告)<br>}<br>

3. 设计意图

虚方法 普通方法
用于实现多态性,允许基类定义通用行为,派生类根据需要自定义实现。例如:
csharp<br>public class Shape {<br> public virtual double Area() => 0;<br>}<br><br>public class Circle : Shape {<br> public override double Area() => Math.PI * Radius * Radius;<br>}<br>
用于实现固定行为,不希望派生类修改方法逻辑。例如:
csharp<br>public class Calculator {<br> public int Add(int a, int b) => a + b; // 不需要重写的方法<br>}<br>

4. 性能差异

  • 虚方法:调用时需要通过虚函数表(VTable)动态查找实际要执行的方法,因此性能略低(但在大多数场景下可以忽略不计)。
  • 普通方法:调用时直接绑定到声明类型的方法,性能更高

5. 语法对比表

特性 虚方法 普通方法
关键字 virtual
能否重写 能(使用 override 不能(只能用 new 隐藏)
多态支持 运行时多态(根据对象实际类型) 编译时绑定(根据声明类型)
默认行为 基类提供默认实现,可被覆盖 行为固定,不可被派生类修改
性能 略低(通过VTable查找) 更高(直接调用)

总结:何时使用?

  • 使用虚方法
    • 当基类希望派生类能够自定义某个方法的实现时。
    • 需要通过基类引用调用派生类方法(实现多态)。
  • 使用普通方法
    • 当方法的逻辑不需要被派生类修改时。
    • 性能敏感的场景(如高频调用的方法)。

通过合理使用虚方法和普通方法,可以在保证代码灵活性的同时,避免不必要的性能开销。

三、迭代器的使用

3.1、使用场景及示例

在这里插入图片描述

在迭代块中,使用yield关键字选择要在foreach循环中使用的值,其语法如下

yield return <value>;

  • 迭代一个类成员(比如方法)IEnumerable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleIterators
{
   class Program
   {
      public static IEnumerable SimpleList()
      {
         yield return "string 1";
         yield return "string 2";
         yield return "string 3";
      }

      static void Main(string[] args)
      {
         foreach (string item in SimpleList())
            Console.WriteLine(item);

         Console.ReadKey();
      }
   }
}


输出
在这里插入图片描述

  • 迭代一个类 Enumerator

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Ch11Ex03
{
   public class Primes
   {
      private long min;
      private long max;

      public Primes()
         : this(2, 100)
      {
      }

      public Primes(long minimum, long maximum)
      {
         if (minimum < 2)
            min = 2;
         else
            min = minimum;

         max = maximum;
      }

      public IEnumerator GetEnumerator()
      {
         for (long possiblePrime = min; possiblePrime <= max; possiblePrime++)
         {
            bool isPrime = true;
            for (long possibleFactor = 2; possibleFactor <=
               (long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++)
            {
               long remainderAfterDivision = possiblePrime % possibleFactor;
               if (remainderAfterDivision == 0)
               {
                  isPrime = false;
                  break;
               }
            }
            if (isPrime)
            {
               yield return possiblePrime;
            }
         }
      }
   }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Ch11Ex03
{
   class Program
   {
      static void Main(string[] args)
      {
         Primes primesFrom2To1000 = new Primes(2, 1000);
       //Primes primesFrom2To1000 = new Primes( );
            foreach (long i in primesFrom2To1000)
            Console.Write("{0} ", i);

         Console.ReadKey();
      }
   }
}


primesFrom2To1000 为定义的一个类,在foreach里迭代这个类,
yield return possiblePrime;
为迭代要输出的值,输出的值的类型为定义的 long possiblePrime

输出如下
在这里插入图片描述

3.2、

四、深度复制与浅度复制

4.1、解析及示例

在C#中,深度复制(Deep Copy)浅度复制(Shallow Copy)是处理对象复制时的两种不同方式,它们的核心区别在于是否递归复制对象的所有成员。以下是详细解释和示例:

1. 浅度复制(Shallow Copy)

  • 定义:创建一个新对象,但只复制对象的顶层成员。对于引用类型的成员,只复制引用(内存地址),而不复制实际对象。
  • 特点
    • 新对象和原对象是不同的实例(内存地址不同)。
    • 引用类型的成员指向同一个对象。
    • 修改引用类型成员会影响所有关联的对象。
  • 实现方式
    • 使用 MemberwiseClone() 方法(受保护,需在类内部实现)。
    • 手动复制每个字段。

示例代码

public class Address
{
    public string City { get; set; }
}

public class Person
{
    public string Name { get; set; } // 值类型
    public Address Address { get; set; } // 引用类型

    // 实现浅复制方法
    public Person ShallowCopy()
    {
        return (Person)this.MemberwiseClone();
    }
}

// 使用示例
Person original = new Person
{
    Name = "张三",
    Address = new Address { City = "北京" }
};

Person shallowCopy = original.ShallowCopy();

// 修改浅复制对象的引用类型成员
shallowCopy.Address.City = "上海";

Console.WriteLine(original.Address.City); // 输出: 上海(被修改了)

2. 深度复制(Deep Copy)

  • 定义:创建一个新对象,并递归复制对象的所有成员。对于引用类型的成员,会创建新的对象实例,而非仅复制引用。
  • 特点
    • 新对象和原对象完全独立,没有共享的引用类型成员。
    • 修改任何一个对象都不会影响其他对象。
  • 实现方式
    • 手动递归复制每个引用类型成员。
    • 使用序列化和反序列化(需类标记为 [Serializable])。

示例代码(手动实现)

public class Address
{
    public string City { get; set; }
    
    // 提供深度复制方法
    public Address DeepCopy()
    {
        return new Address { City = this.City };
    }
}

public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }

    // 实现深度复制方法
    public Person DeepCopy()
    {
        return new Person
        {
            Name = this.Name,
            Address = this.Address.DeepCopy() // 递归复制引用类型
        };
    }
}

// 使用示例
Person original = new Person
{
    Name = "张三",
    Address = new Address { City = "北京" }
};

Person deepCopy = original.DeepCopy();

// 修改深度复制对象的引用类型成员
deepCopy.Address.City = "上海";

Console.WriteLine(original.Address.City); // 输出: 北京(未被修改)

示例代码(使用序列化)

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable] // 必须标记为可序列化
public class Address
{
    public string City { get; set; }
}

[Serializable]
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }

    // 使用序列化实现深度复制
    public Person DeepCopy()
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, this);
            stream.Position = 0;
            return (Person)formatter.Deserialize(stream);
        }
    }
}
  1. 对比表格
    | 特性 | 浅度复制 | 深度复制 |
    |------------------------|------------------------------|------------------------------|
    | 新对象实例 | 创建顶层对象 | 创建所有层级的对象 |
    | 引用类型成员 | 共享同一个实例 | 创建新实例 |
    | 值类型成员 | 复制值 | 复制值 |
    | 实现复杂度 | 低(使用 MemberwiseClone) | 高(递归或序列化) |
    | 修改影响 | 影响所有共享引用的对象 | 仅影响当前对象 |
    | 性能 | 高(仅复制引用) | 低(需创建多个对象) |

  2. 常见问题

  • 循环引用:深度复制时需小心处理循环引用,可能导致栈溢出。
  • 不可序列化类型:使用序列化方法时,所有成员必须可序列化。
  • 性能开销:深度复制涉及创建多个对象,对性能有影响。
  1. 选择建议
  • 使用浅度复制:当对象的引用类型成员是不可变的,或不需要独立修改时。
  • 使用深度复制:当需要完全独立的对象,避免修改相互影响时。

总结

  • 浅度复制:复制顶层对象,共享引用类型成员。
  • 深度复制:递归复制所有成员,创建完全独立的对象。

理解这两种复制方式的区别,有助于避免在代码中出现意外的副作用,并根据需求选择合适的复制策略。

五、引用和值类型

在C#中,变量类型分为值类型引用类型,它们在内存存储、传递方式和生命周期等方面有本质区别。以下是常见的值类型和引用类型及其特点:

一、值类型(Value Types)

值类型变量直接存储数据值,通常分配在栈(Stack)上(局部变量)或结构体中。值类型的复制会创建独立的副本。

  1. 内置值类型
分类 类型 示例
整数 byte, sbyte, short, ushort, int, uint, long, ulong int age = 30;
浮点数 float, double, decimal double price = 9.99;
布尔 bool bool isActive = true;
字符 char char letter = 'A';
枚举 enum(自定义) enum Color { Red, Green, Blue };
元组 (int, string)(C# 7.0+) var person = (1, "Alice");
  1. 结构体(Struct)
    结构体是用户自定义的值类型,常用于轻量级数据存储:
public struct Point
{
    public int X;
    public int Y;
}

Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // 复制值,p2与p1独立
  1. 可空值类型(Nullable)
    允许值类型变量存储 null
int? nullableInt = null; // 可空整数
bool? nullableBool = false;

二、引用类型(Reference Types)

引用类型变量存储对象的内存地址(引用),对象本身分配在堆(Heap)上。引用类型的复制仅传递引用,多个变量可能指向同一对象。

  1. 内置引用类型
分类 类型 示例
字符串 string string name = "John";
数组 T[](任意类型的数组) int[] numbers = new int[5];
集合 List<T>, Dictionary<TKey, TValue>, HashSet<T> List<string> names = new List<string>();
  1. 类(Class)
    类是最常见的引用类型,包括自定义类和框架类:
public class Person
{
    public string Name { get; set; }
}

Person p1 = new Person { Name = "Alice" };
Person p2 = p1; // 复制引用,p2和p1指向同一对象
  1. 接口(Interface)
    接口本身不能实例化,但实现接口的类是引用类型:
public interface IAnimal
{
    void Speak();
}

public class Dog : IAnimal
{
    public void Speak() => Console.WriteLine("Woof!");
}

IAnimal animal = new Dog(); // 引用类型
  1. 委托(Delegate)
    委托是方法的类型安全引用,属于引用类型:
public delegate void MyDelegate(string message);

MyDelegate del = Console.WriteLine; // 委托实例
  1. 对象(Object)
    所有类型的基类,可引用任何类型的对象:
object obj = "Hello"; // 引用字符串对象
obj = 123; // 引用整数对象(装箱)
  1. 动态类型(Dynamic)
    在运行时确定类型,属于引用类型:
dynamic dynamicVar = "Hello";
dynamicVar = 123; // 运行时有效

三、关键区别总结

特性 值类型 引用类型
存储位置 栈或结构体
复制方式 创建独立副本 复制引用(共享对象)
默认值 0, false, null(可空类型) null
基类 System.ValueType System.Object
常见类型 基本数据类型、结构体、枚举 类、接口、数组、字符串、委托

四、特殊注意事项

  1. 字符串的不可变性string 是引用类型,但由于不可变性,赋值时看似创建了副本:

    string a = "Hello";
    string b = a; // 复制引用,但字符串不可变
    b = "World";  // b指向新字符串,a不受影响
    
  2. 装箱与拆箱:值类型与 object 之间的转换会产生性能开销:

    int num = 100;
    object boxed = num; // 装箱(值类型→引用类型)
    int unboxed = (int)boxed; // 拆箱(引用类型→值类型)
    
  3. 结构体与类的选择

    • 结构体:轻量级、频繁创建/销毁、数据独立。
    • :复杂行为、需要继承、共享状态。

理解值类型和引用类型的区别是编写高效、安全C#代码的基础。根据场景选择合适的类型,可以避免内存泄漏、提高性能并减少错误。


网站公告

今日签到

点亮在社区的每一天
去签到