C#面向对象之抽象,接口,运算符重载,属性,索引器

1 抽象

1.1 抽象方法

1.1.1 抽象方法

使用关键字 abstract 创建抽象类,用于提供接口的部分类的实现。当一个派生类继承自该抽象类时,实现即完成。抽象类包含抽象方法,抽象方法可被派生类实现。派生类具有更专业的功能。

请注意,下面是有关抽象类的一些规则:

  • 不能创建一个抽象类的实例。
  • 不能在一个抽象类外部声明一个抽象方法。
  • 通过在类定义前面放置关键字 sealed,可以将类声明为密封类。当一个类被声明为 sealed 时,它不能被继承。抽象类不能被声明为 sealed

1.1.2 虚方法

当有一个定义在类中的函数需要在继承类中实现时,可以使用虚方法

  • 虚方法是使用关键字 virtual 声明的
  • 虚方法必须有实现部分,即使这个实现只是空操作(例如,仅包含一对大括号)
  • 虚方法可以在不同的继承类中有不同的实现
  • 对虚方法的调用是在运行时发生的
  • C#动态多态性是通过 抽象类虚方法 实现的

1.1.3 new

在 C# 中,new 关键字可以用于隐藏基类中的方法。这与 override 不同,new 不会改变虚方法的行为,而是提供一个新的实现,但不会影响多态行为。

public class BaseClass
{
    public virtual void MyMethod()
    {
        Console.WriteLine("BaseClass.MyMethod");
    }
}
public class DerivedClass : BaseClass
{
    public new void MyMethod()
    {
        Console.WriteLine("DerivedClass.MyMethod");
    }
}
public class Program
{
    public static void Main()
    {
        BaseClass baseObj = new DerivedClass();
        baseObj.MyMethod(); // 输出: BaseClass.MyMethod

        DerivedClass derivedObj = new DerivedClass();
        derivedObj.MyMethod(); // 输出: DerivedClass.MyMethod
    }
}

1.2 抽象属性

抽象类可拥有抽象属性,这些属性应在派生类中被实现。下面的程序说明了这点:

using System;
namespace Runoob
{
    public abstract class Person
    {
        public abstract string Name { get; set; }
        public abstract int Age { get; set; }
    }

    class Student : Person
    {
        // 声明自动实现的属性 并指定属性的默认值
        public string Code { get; set; } = "N.A";
        public override string Name { get; set; } = "N.A";
        public override int Age { get; set; } = 0;

        public override string ToString()
        {
            return $"Code = {Code}, Name = {Name}, Age = {Age}";
        }
    }

    class ExampleDemo
    {
        public static void Main()
        {
            // 创建一个新的 Student 对象
            Student s = new Student
            {
                Code = "001",
                Name = "Zara",
                Age = 9
            };

            Console.WriteLine("Student Info:- {0}", s);
           
            // 增加年龄
            s.Age += 1;
            Console.WriteLine("Student Info:- {0}", s);

            Console.ReadKey();
        }
    }
}

1.3 抽象示例

抽象方法

using System;
namespace PolymorphismApplication
{
   abstract class Shape
   {
       abstract public int area();
   }
   class Rectangle:  Shape
   {
      private int length;
      private int width;
      public Rectangle( int a=0, int b=0)
      {
         length = a;
         width = b;
      }
      public override int area ()
      { 
         Console.WriteLine("Rectangle 类的面积:");
         return (width * length); 
      }
   }
   class RectangleTester
   {
      static void Main(string[] args)
      {
         Rectangle r = new Rectangle(10, 7);
         double a = r.area();
         Console.WriteLine("面积: {0}",a);
         Console.ReadKey();
      }
   }
}

虚方法

using System;
using System.Collections.Generic;

public class Shape
{
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }
   
    // 虚方法
    public virtual void Draw()
    {
        Console.WriteLine("执行基类的画图任务");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个圆形");
        base.Draw();
    }
}
class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个长方形");
        base.Draw();
    }
}
class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个三角形");
        base.Draw();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle
        var shapes = new List<Shape>
        {
            new Rectangle(),
            new Triangle(),
            new Circle()
        };

        // 使用 foreach 循环对该列表的派生类进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法 
        foreach (var shape in shapes)
        {
            shape.Draw();
        }
        Console.WriteLine("按下任意键退出。");
        Console.ReadKey();
    }
}

2 接口

2.1 定义

接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。

接口使得实现接口的类或结构在形式上保持一致。
抽象类在某种程度上与接口类似,但是,它们大多只是用在当只有少数方法由基类声明由派生类实现时。
接口本身并不实现任何功能,它只是和声明实现该接口的对象订立一个必须实现哪些行为的契约。
接口使用 interface 关键字声明,它与类的声明类似。接口声明默认是 public 的,通常接口命名以 I 字母开头,

2.2 简单使用

2.2.1 声明使用接口

声明接口

interface IMyInterface
{
    void MethodToImplement();
}

这个接口只有一个方法 MethodToImplement(),没有参数和返回值,当然我们可以按照需求设置参数和返回值。
值得注意的是,该方法并没有具体的实现。接口声明的实例:

using System;

interface IMyInterface
{
        // 接口成员
    void MethodToImplement();
}

class InterfaceImplementer : IMyInterface
{
    static void Main()
    {
        InterfaceImplementer iImp = new InterfaceImplementer();
        iImp.MethodToImplement();
    }

    public void MethodToImplement()
    {
        Console.WriteLine("MethodToImplement() called.");
    }
}

2.2.2 接口继承

以下实例定义了两个接口 IMyInterface 和 IParentInterface。
如果一个接口继承其他接口,那么实现类或结构就需要实现所有接口的成员。

以下实例 IMyInterface 继承了 IParentInterface 接口,因此接口实现类必须实现 MethodToImplement() 和 ParentInterfaceMethod() 方法:

using System;

interface IParentInterface
{
    void ParentInterfaceMethod();
}

interface IMyInterface : IParentInterface
{
    void MethodToImplement();
}

class InterfaceImplementer : IMyInterface
{
    static void Main()
    {
        InterfaceImplementer iImp = new InterfaceImplementer();
        iImp.MethodToImplement();
        iImp.ParentInterfaceMethod();
    }

    public void MethodToImplement()
    {
        Console.WriteLine("MethodToImplement() called.");
    }

    public void ParentInterfaceMethod()
    {
        Console.WriteLine("ParentInterfaceMethod() called.");
    }
}
实例输出结果为:

MethodToImplement() called.
ParentInterfaceMethod() called.

2.3 接口 显式实现和隐式实现

接口的显式实现隐式实现是两种实现接口成员的方式。它们的主要区别在于成员的访问方式、适用场景以及实现的语法

隐式实现 vs 显式实现

特性 隐式实现 显式实现
访问方式 可以通过类的实例直接访问 必须通过接口引用访问
成员可见性 接口成员实现后是 public,类的公共 API 接口成员实现后是类的私有成员,不暴露
解决冲突能力 无法解决多个接口中同名成员的冲突 能够为每个接口提供独立的实现
适用场景 简单场景,接口成员是类的公共 API 时使用 多接口实现或需要隐藏接口成员时使用

2.3.1 隐式实现

特点:

  • 接口的成员被实现为类或结构的公共成员(public)。
  • 实现后的接口成员可以通过类的实例直接访问。
  • 如果一个类实现了多个接口,且接口中的成员名称相同,会引发成员冲突,无法使用隐式实现

适用场景:

  • 接口方法需要被作为类的公共 API 供外部调用时。
  • 简单场景,且类只实现单一接口时。
interface IExample
{
    void DoWork();
}

class Example : IExample
{
    // 隐式实现接口成员
    public void DoWork()
    {
        Console.WriteLine("DoWork executed");
    }
}

class Program
{
    static void Main()
    {
        Example example = new Example();
        example.DoWork(); // 可以直接访问
    }
}

2.3.2 显式实现

特点:

  • 接口成员通过显式实现,不会暴露为类的公共成员。
  • 接口的成员只能通过接口引用调用,不能通过类的实例直接访问。
  • 显式实现常用于解决多个接口的成员名称冲突的问题。

适用场景:

  • 当类实现多个接口,且接口成员名称冲突时。
  • 不希望接口方法作为类的公共 API 暴露,而是希望仅通过接口使用这些方法。
interface IExample
{
    void DoWork();
}

class Example : IExample
{
    // 显式实现接口成员
    void IExample.DoWork()
    {
        Console.WriteLine("DoWork executed");
    }
}

class Program
{
    static void Main()
    {
        Example example = new Example();
        // example.DoWork(); // 编译错误,无法通过类实例访问
        //var example = new Example();// 也是编译错误

        // 需要通过接口引用访问
        IExample iExample = example;
        iExample.DoWork(); // 正确
    }
}

2.3.3 多接口实现中的应用

场景:解决多个接口的成员冲突
当类实现多个接口,而接口中存在同名成员时,可以通过显式实现为每个接口提供独立的实现。

interface IExampleA
{
    void DoWork();
}

interface IExampleB
{
    void DoWork();
}

class Example : IExampleA, IExampleB
{
    // 显式实现 IExampleA 的 DoWork 方法
    void IExampleA.DoWork()
    {
        Console.WriteLine("IExampleA.DoWork executed");
    }

    // 显式实现 IExampleB 的 DoWork 方法
    void IExampleB.DoWork()
    {
        Console.WriteLine("IExampleB.DoWork executed");
    }
}

class Program
{
    static void Main()
    {
        Example example = new Example();

        // 使用 IExampleA 的 DoWork 方法
        IExampleA exampleA = example;
        exampleA.DoWork(); // 输出:IExampleA.DoWork executed

        // 使用 IExampleB 的 DoWork 方法
        IExampleB exampleB = example;
        exampleB.DoWork(); // 输出:IExampleB.DoWork executed
    }
}

3 运算符重载 operator

3.1 简介

重定义或重载 C# 中内置的运算符,也可以使用用户自定义类型的运算符。重载运算符是具有特殊名称的函数,是通过关键字 operator 后跟运算符的符号来定义的。与其他函数一样,重载运算符有返回类型和参数列表。
必须是静态方法 和 operator 关键字

可重载和不可重载运算符

运算符 描述
+, -, !, ~, ++, -- 一元运算符只有一个操作数,且可以被重载
+, -, *, /, % 这些二元运算符带有两个操作数,且可以被重载
==, !=, <, >, <=, >= 这些比较运算符可以被重载
&&, || 这些条件逻辑运算符不能被直接重载
+=, -=, *=, /=, %= 这些赋值运算符不能被重载
=, ., ?:, ->, new, is, sizeof, typeof 这些运算符不能被重载

注意

  • 运算符只能采用值参数,不能采用 refout 参数。
  • C# 要求成对重载比较运算符。如果重载了==,则也必须重载!=,否则产生编译错误。同时,比较运算符必须返回bool类型的值,这是与其他算术运算符的根本区别。
  • C# 不允许重载=运算符,但如果重载例如+运算符,编译器会自动使用+运算符的重载来执行+=运算符的操作。
  • 运算符重载的其实就是函数重载。首先通过指定的运算表达式调用对应的运算符函数,然后再将运算对象转化为运算符函数的实参,接着根据实参的类型来确定需要调用的函数的重载,这个过程是由编译器完成。

3.2 示例

using System;
namespace OperatorOvlApplication
{
   class Box
   {
      private double length;      // 长度
      private double breadth;     // 宽度
      private double height;      // 高度

      public double getVolume()
      {
         return length * breadth * height;
      }
      public void setLength( double len )
      {
         length = len;
      }

      public void setBreadth( double bre )
      {
         breadth = bre;
      }

      public void setHeight( double hei )
      {
         height = hei;
      }
      // 重载 + 运算符来把两个 Box 对象相加
      public static Box operator+ (Box b, Box c)
      {
         Box box = new Box();
         box.length = b.length + c.length;
         box.breadth = b.breadth + c.breadth;
         box.height = b.height + c.height;
         return box;
      }

   }

   class Tester
   {
      static void Main(string[] args)
      {
         Box Box1 = new Box();         // 声明 Box1,类型为 Box
         Box Box2 = new Box();         // 声明 Box2,类型为 Box
         Box Box3 = new Box();         // 声明 Box3,类型为 Box
         double volume = 0.0;          // 体积

         // Box1 详述
         Box1.setLength(6.0);
         Box1.setBreadth(7.0);
         Box1.setHeight(5.0);

         // Box2 详述
         Box2.setLength(12.0);
         Box2.setBreadth(13.0);
         Box2.setHeight(10.0);

         // Box1 的体积
         volume = Box1.getVolume();
         Console.WriteLine("Box1 的体积: {0}", volume);

         // Box2 的体积
         volume = Box2.getVolume();
         Console.WriteLine("Box2 的体积: {0}", volume);

         // 把两个对象相加
         Box3 = Box1 + Box2;
      }
   }
}

4 属性

4.1 简介

C# 中的属性(Property)是结构体中用于封装数据的成员。它们提供了一种方式来定义类成员的访问和设置规则,通常用于隐藏字段(Fields) 的内部实现细节,同时提供控制数据访问的机制。
属性可以看作是对字段的包装器,通常由 getset 访问器组成。
属性(Property)不会确定存储位置。相反,它们具有可读写或计算它们值的 访问器(accessors)
属性中有个特殊变量 value,但是 get 访问器不能直接返回 valuevalueset 访问器中的隐式参数,用于接收传入的值。get 访问器没有 value 参数,因此不能直接返回 value

4.2 基本语法

例如,有一个名为 Student 的类,带有 age、name 和 code 的私有域。我们不能在类的范围以外直接访问这些域,但是我们可以拥有访问这些私有域的属性。

public class Person
{
    private string name;//这是字段

    public string Name//这是属性
    {
        get { return name; }
        set { name = value; }
    }
}

以上代码中,Name 属性封装了私有字段 nameget 访问器用于获取字段值,而 set 访问器用于设置字段值。

4.3 自动实现的属性

如果需要一个简单的属性,C# 使用自动实现的属性,这样就不需要显式地定义字段。

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

在这种情况下,编译器会自动为 Name 属性 生成一个私有的匿名字段来存储值。

4.4 计算属性

属性也可以直接计算的,不依赖于字段

public class Rectangle
{
    public int Width { get; set; }
    public int Height { get; set; }

    public int Area
    {
        get { return Width * Height; }
    }
}

4.5 访问器(Accessors)

属性(Property)的访问器(accessor)包含有助于获取(读取或计算)或设置(写入)属性的可执行语句。访问器(accessor)声明可包含一个 get 访问器、一个 set 访问器,或者同时包含二者。例如:

// 声明类型为 string 的 Code 属性
public string Code
{
   get
   {
      return code;
   }
   set
   {
      code = value;
   }
}


}

下面的实例演示了属性(Property)的用法:

using System;
namespace runoob
{
   class Student
   {

      private string code = "N.A";
      private string name = "not known";
      private int age = 0;

      // 声明类型为 string 的 Code 属性
      public string Code
      {
         get
         {
            return code;
         }
         set
         {
            code = value;
         }
      }
   
      // 声明类型为 string 的 Name 属性
      public string Name
      {
         get
         {
            return name;
         }
         set
         {
            name = value;
         }
      }

      // 声明类型为 int 的 Age 属性
      public int Age
      {
         get
         {
            return age;
         }
         set
         {
            age = value;
         }
      }
      public override string ToString()
      {
         return "Code = " + Code +", Name = " + Name + ", Age = " + Age;
      }
    }
    class ExampleDemo
    {
      public static void Main()
      {
         // 创建一个新的 Student 对象
         Student s = new Student();
           
         // 设置 student 的 code、name 和 age
         s.Code = "001";
         s.Name = "Zara";
         s.Age = 9;
         Console.WriteLine("Student Info: {0}", s);
         // 增加年龄
         s.Age += 1;
         Console.WriteLine("Student Info: {0}", s);
         Console.ReadKey();
       }
   }
}

4.6 与Lambda结合

C# 6.0 开始,可以通过表达式主体(Expression-bodied)成员语法来简化属性的实现,特别是只读属性和计算属性。

public class ExampleClass
{
    private int _myProperty;
    
    // 使用 Lambda 语法
    public int MyProperty
    {
        get => _myProperty;
        set => _myProperty = value;
    }
}

注意

  • 只读属性与线程安全:
    使用 lambda 定义只读属性时,确保属性访问的字段或资源是线程安全的。
  • 避免复杂逻辑:
    虽然可以在属性中使用复杂逻辑,但建议将复杂逻辑封装到方法中,以提高可读性和可维护性。
  • 支持多行逻辑的表达式:
    Lambda 属性仅支持单行表达式。如果逻辑超过一行,则应使用完整的 getter 方法

4.6.1 使用 Lambda 表达式定义只读属性

只读属性是指没有 setter 的属性,常用于计算或返回固定的值。

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    // 使用 Lambda 表达式定义只读属性
    public string FullName => $"{FirstName} {LastName}";
}

FullName 是一个只读属性,使用 lambda 表达式简化了 getter 的实现。
=> 表示属性的 getter 返回一个计算值。

var person = new Person { FirstName = "John", LastName = "Doe" };
Console.WriteLine(person.FullName); // 输出:John Doe

4.6.2 使用 Lambda 表达式定义计算属性

计算属性可以动态地基于其他字段或属性计算值。

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

    // 使用 Lambda 定义计算属性
    public double Area => Math.PI * Radius * Radius;
}

Area 是一个计算属性,动态计算圆的面积。
无需手动定义完整的 getter 方法。

4.6.3 结合自动属性与 Lambda 表达式

自动属性(Auto-Implemented Properties)可以直接结合 lambda 表达式,进一步简化属性定义。

public class Product
{
    public decimal Price { get; set; }
    public int Quantity { get; set; }

    // Lambda 定义计算属性
    public decimal TotalCost => Price * Quantity;
}

4.6.4 复杂逻辑场景

当属性需要使用复杂逻辑计算时,也可以通过 lambda 调用私有方法,保持属性定义简洁。

public class Employee
{
    public int HoursWorked { get; set; }
    public decimal HourlyRate { get; set; }

    // 调用私有方法的属性
    public decimal Salary => CalculateSalary();

    private decimal CalculateSalary()
    {
        return HoursWorked * HourlyRate;
    }
}

5 索引器

索引器(Indexer) 允许一个对象可以像数组一样使用下标的方式来访问。
当为类定义一个索引器时,该类的行为就会像一个 虚拟数组(virtual array) 一样。可以使用数组访问运算符 [ ] 来访问该类的的成员。

5.1 语法

一维索引器的语法如下:

element-type this[int index]
{
   // get 访问器
   get
   {
      // 返回 index 指定的值
   }

   // set 访问器
   set
   {
      // 设置 index 指定的值
   }
}

5.2 索引器 用途

索引器的行为的声明在某种程度上类似于属性(property)。就像属性(property),可使用 getset 访问器来定义索引器。但是,属性返回或设置一个特定的数据成员,而索引器返回或设置对象实例的一个特定值。换句话说,它把实例数据分为更小的部分,并索引每个部分,获取或设置每个部分。

定义一个属性(property)包括提供属性名称。索引器定义的时候不带有名称,但带有 this 关键字,它指向对象实例。下面的实例演示了这个概念:

using System;
namespace IndexerApplication
{
   class IndexedNames
   {
      private string[] namelist = new string[size];
      static public int size = 10;
      public IndexedNames()
      {
         for (int i = 0; i < size; i++)
         namelist[i] = "N. A.";
      }
      public string this[int index]
      {
         get
         {
            string tmp;

            if( index >= 0 && index <= size-1 )
            {
               tmp = namelist[index];
            }
            else
            {
               tmp = "";
            }

            return tmp ;
         }
         set
         {
            if( index >= 0 && index <= size-1 )
            {
               namelist[index] = value;
            }
         }
      }

      static void Main(string[] args)
      {
         IndexedNames names = new IndexedNames();
         names[0] = "Zara";
         names[1] = "Riz";
         names[2] = "Nuha";
         names[3] = "Asif";
         names[4] = "Davinder";
         names[5] = "Sunil";
         names[6] = "Rubic";
         for ( int i = 0; i < IndexedNames.size; i++ )
         {
            Console.WriteLine(names[i]);
         }
         Console.ReadKey();
      }
   }
}

5.3 重载索引器(Indexer)

索引器(Indexer)可被重载。索引器声明的时候也可带有多个参数,且每个参数可以是不同的类型。没有必要让索引器必须是整型的。C# 允许索引器可以是其他类型,例如,字符串类型。

下面的实例演示了重载索引器:

using System;
namespace IndexerApplication
{
   class IndexedNames
   {
      private string[] namelist = new string[size];
      static public int size = 10;
      public IndexedNames()
      {
         for (int i = 0; i < size; i++)
         {
          namelist[i] = "N. A.";
         }
      }
      public string this[int index]
      {
         get
         {
            string tmp;

            if( index >= 0 && index <= size-1 )
            {
               tmp = namelist[index];
            }
            else
            {
               tmp = "";
            }

            return ( tmp );
         }
         set
         {
            if( index >= 0 && index <= size-1 )
            {
               namelist[index] = value;
            }
         }
      }
      public int this[string name]
      {
         get
         {
            int index = 0;
            while(index < size)
            {
               if (namelist[index] == name)
               {
                return index;
               }
               index++;
            }
            return index;
         }

      }

      static void Main(string[] args)
      {
         IndexedNames names = new IndexedNames();
         names[0] = "Zara";
         names[1] = "Riz";
         names[2] = "Nuha";
         names[3] = "Asif";
         names[4] = "Davinder";
         names[5] = "Sunil";
         names[6] = "Rubic";
         // 使用带有 int 参数的第一个索引器
         for (int i = 0; i < IndexedNames.size; i++)
         {
            Console.WriteLine(names[i]);
         }
         // 使用带有 string 参数的第二个索引器
         Console.WriteLine(names["Nuha"]);
         Console.ReadKey();
      }
   }
}
posted @ 2024-12-01 17:25  上善若泪  阅读(17)  评论(0编辑  收藏  举报