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 | 这些运算符不能被重载 |
注意
:
- 运算符只能采用值参数,不能采用
ref
或out
参数。 - 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)
的内部实现细节,同时提供控制数据访问的机制。
属性可以看作是对字段
的包装器,通常由 get
和 set
访问器组成。
属性(Property
)不会确定存储位置。相反,它们具有可读写或计算它们值的 访问器(accessors)
。
属性中有个特殊变量 value
,但是 get
访问器不能直接返回 value
,value
是 set
访问器中的隐式参数,用于接收传入的值。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
属性封装了私有字段 name
。get
访问器用于获取字段值,而 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
),可使用 get
和 set
访问器来定义索引器。但是,属性返回或设置一个特定的数据成员,而索引器返回或设置对象实例的一个特定值。换句话说,它把实例数据分为更小的部分,并索引每个部分,获取或设置每个部分。
定义一个属性(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();
}
}
}