C# 固定大小缓冲区与运算符重载详解
1. 固定大小缓冲区
在 C# 中,固定大小缓冲区可像数组一样聚合在结构体中,但结构体支持使用固定大小缓冲区,类则不支持。使用固定大小缓冲区时,需遵循以下规则:
- 仅在非安全上下文中可用。
- 只能表示一维数组(向量)。
- 数组必须有特定长度。
- 仅允许在
struct
类型中使用。
- 仅涉及
bool
、
byte
、
char
、
short
、
int
、
long
、
sbyte
、
ushort
、
uint
、
ulong
、
float
或
double
类型。
固定大小缓冲区的语法如下:
attributs accessibilité modificateur fixed type identificateur [expression]
以下是使用
fixed
关键字的
OSVERSIONINFO
结构体示例代码:
public class API {
[StructLayout(LayoutKind.Sequential)]
unsafe public struct OSVERSIONINFO {
public System.Int32 dwOSVersionInfoSize;
public System.Int32 dwMajorVersion;
public System.Int32 dwMinorVersion;
public System.Int32 dwBuildNumber;
public System.Int32 dwPlatformId;
public fixed char szCSDVersion[128];
}
}
1.1 指针与非安全代码
虽然 C# 语言通常不提供指针,但开发者仍可在非安全代码上下文中使用。使用指针时,需注意以下几点:
- 指针使用限于非安全代码,代码中必须出现
unsafe
关键字。
- 可创建指向非托管类型(如
int
、
float
和
char
类型)的指针。
- 非安全代码必须使用
unsafe
编译器选项进行编译。
1.2 内存管理与
fixed
语句
不能创建指向可移动内存的指针,因为托管内存可移动且由垃圾回收器(GC)管理。
fixed
语句可在一个代码块期间固定托管内存,使固定后的内存不再受 GC 影响。
1.3 函数导入与互操作性
DllImportAttribute
用于描述从库中导出的非托管函数,包含多种配置函数导入的选项。在函数调用期间,互操作性封送器会在托管内存和非托管内存之间封送参数和返回值。其中,可直接封送的类型无需转换,不可直接封送的类型则需转换为非托管内存。开发者可使用
MarshalAs
属性显式封送不可直接封送的类型。
2. 运算符重载概述
开发者可对用户定义类型进行运算符重载,以描述该类型与常用运算符的交互方式。内置类型(如
int
、
float
和长值类型)能自动支持大多数运算符,使用条件、逻辑和数学运算符时表现直观。但 C# 编译器无法直接在标准运算符上下文中使用用户定义的类,此时需要通过运算符重载来协助编译器理解。
运算符重载适用于表示数字系统或执行数学运算的用户定义类型,如分数、复数、总和、十六进制数字系统等。此外,还可通过运算符重载描述用户定义类型之间的转换。
运算符重载应直观且避免混淆,例如
System.String
对
operator+
进行重载以实现字符串连接,这与加法概念类似。若重载
operator+
导致截断字符,则会造成混淆。运算符重载不宜过度使用,并非适用于所有情况。若不想使用运算符重载,可实现命名方法(如
Add
代替
operator+
,
Subtract
代替
operator-
),命名方法调用更明确,而运算符函数调用较隐式。
3. 逻辑和数学运算符重载
3.1 不可重载的运算符
逻辑和数学运算符大多为一元或二元运算符,用户定义类型可对其进行重载,但条件运算符(唯一的三元运算符)不可重载。以下是用户定义类型中不可重载的运算符列表:
- 点运算符(
identifier.member
)
- 调用运算符(
methodname()
)
- 赋值运算符(
=
、
+=
、
/=
、
%=
等)
- 条件运算符(
&&
、
||
和
?:
)
-
checked
和
unchecked
运算符
-
new
运算符
-
typeof
运算符
-
as
和
is
运算符
- 数组运算符(
[]
)
- 指针运算符
赋值运算符(如
+=
和
/=
)可通过重载单个运算符隐式重载。此外,开发者不能使用运算符重载定义新运算符。
3.2 C++ 与 C# 运算符重载差异
C++ 中可重载赋值运算符、
new
运算符和数组运算符,但 C# 不允许。从 C++ 迁移代码到 C# 时需注意,可通过实现
ICloneable
接口替代赋值运算符重载。由于垃圾回收器负责管理动态内存,
new
运算符在托管代码中不可重载。在 C++ 中,数组运算符重载常用于创建安全数组以控制边界错误,但在 C# 中,CLR 会自动检测这些错误,减少了重载数组运算符的必要性。
3.3 运算符重载的特性
运算符重载会重新定义运算符在表达式中的行为,但不会改变运算符的语法、优先级或结合性。例如,重载除法运算符后,其语法仍为
obj1/obj2
,优先级和结合性也保持不变。
3.4 运算符重载的实现
重载运算符需实现为静态公共函数。一元运算符有一个操作数,二元运算符有两个操作数,且重载运算符的参数不能是
ref
或
out
。
一元或二元运算符重载方法的语法如下:
public static type operator unaire(typeclasse opérande)
public static type operator binaire(type opérandelhs, type opéranderhs)
对于一元运算符方法,操作数类型与包含类相同;对于二元运算符方法,至少一个参数必须是包含类型,这样类型才能在表达式中作为左操作数(lhs)、右操作数(rhs)或同时作为两者出现。返回类型可以是除
void
外的任何值类型。
以下是一个
ZClass
类重载
operator+
的示例:
using System;
namespace Donis.CSharpBook{
public class Starter{
public static void Main(){
ZClass obj1=new ZClass(5,10);
ZClass obj2=new ZClass(15,20);
ZClass obj3=obj1+obj2;
Console.WriteLine(obj3.Total);
}
}
class ZClass {
public ZClass(int _fielda, int _fieldb) {
fielda=_fielda;
fieldb=_fieldb;
}
public static ZClass operator+(ZClass lhs, ZClass rhs) {
return new ZClass(lhs.fielda+rhs.fielda,
lhs.fieldb+rhs.fieldb);
}
public int Total {
get {
return fielda+fieldb;
}
}
protected int fielda, fieldb;
}
}
3.5 派生类中的运算符重载
运算符方法在派生类中可用,运算符函数至少有一个操作数为基类型。可将派生类实例替换为基类型操作数并访问基类型成员,但返回基类型实例给派生类型变量时可能会在运行时出错。若要在派生类中改变运算符行为,可重新实现运算符函数。
以下是
YClass
派生自
ZClass
并进行运算符重载的示例:
namespace Donis.CSharpBook{
public class Starter{
public static void Main(){
YClass obj1=new YClass(5,10);
YClass obj2=new YClass(15,20);
ZClass obj3=obj1+obj2;
Console.WriteLine(obj3.Total);
YClass obj4=obj1-obj2;
Console.WriteLine(obj4.Total);
}
}
// Partial listing
public class YClass:ZClass {
public YClass(int _fielda, int _fieldb)
:base(_fielda, _fieldb) {
}
public static YClass operator-(YClass lhs, YClass rhs) {
return new YClass(lhs.fielda-rhs.fielda,
lhs.fieldb-rhs.fieldb);
}
}
}
3.6 多次重载运算符
可对成员运算符函数进行多次重载,每次重载的函数签名必须唯一。操作数的隐式转换在一定程度上可减少运算符重载的需求。
以下是
operator+
被重载三次的示例:
public static ZClass operator+(ZClass lhs, ZClass rhs) {
return new ZClass(lhs.fielda+rhs.fielda,
lhs.fieldb+rhs.fieldb);
}
public static ZClass operator+(ZClass lhs, int rhs) {
return new ZClass(lhs.fielda+rhs,
lhs.fieldb+rhs);
}
public static ZClass operator+(int lhs, ZClass rhs) {
return new ZClass(lhs+rhs.fielda,
lhs+rhs.fieldb);
}
使用示例:
obj3=obj1+obj2;
obj1=obj1+10;
obj2=20+obj2;
3.7 并行命名方法
由于运算符重载未在公共语言规范(CLS)中规定,并非所有托管语言都支持。因此,建议为每个成员运算符函数实现并行命名方法,命名方法可调用成员运算符函数。
以下是
ZClass
类实现并行命名方法的示例:
public class ZClass {
public ZClass(int _fielda, int _fieldb) {
fielda=_fielda;
fieldb=_fieldb;
}
public static ZClass operator+(ZClass lhs, ZClass rhs) {
return new ZClass(lhs.fielda+rhs.fielda,
lhs.fieldb+rhs.fieldb);
}
public ZClass Add(ZClass rhs) {
return this+rhs;
}
public int Total {
get {
return fielda+fieldb;
}
}
protected int fielda, fieldb;
}
3.8 运算符重载的语义规则
运算符重载的语义确保逻辑和数学运算符的唯一性,重载这些运算符时需遵循特殊规则。
3.9 增量和减量运算符重载
增量(
++
)和减量(
--
)运算符是一元运算符,操作数和返回类型必须是包含类或其派生类。为保持运算符的底层行为,增量和减量运算符应返回当前对象的修改实例,也可返回全新实例,但重载不应改变运算符的基本含义。重载增量或减量运算符会同时影响其前缀和后缀使用方式。
以下是
ZClass
类重载增量和减量运算符的示例:
public class ZClass {
public ZClass(int _fielda, int _fieldb) {
fielda=_fielda;
fieldb=_fieldb;
}
public static ZClass operator++(ZClass curr) {
++curr.fielda;
++curr.fieldb;
return curr;
}
public static ZClass operator--(ZClass curr) {
--curr.fielda;
--curr.fieldb;
return curr;
}
public int Total {
get {
return fielda+fieldb;
}
}
public int fielda, fieldb;
}
3.10 左移和右移运算符重载
左移和右移运算符通常执行二进制移位操作。重载这些运算符时,第一个操作数必须与包含类类型相同,第二个操作数必须是
int
类型,表示移位的幅度。返回类型可以是除
void
外的任何元素。左移和右移运算符可独立实现,但建议同时实现,因为它们之间存在逻辑联系。
综上所述,运算符重载为用户定义类型提供了更灵活的使用方式,但在使用时需遵循相关规则和原则,确保代码的可读性和可维护性。通过合理运用运算符重载和配套的命名方法,可使代码更加直观和易于理解。
4. 转换运算符重载
4.1 转换运算符的作用
转换运算符可用于描述用户定义类型之间的转换,包括将用户定义类型转换为内置类型或其他用户定义类型。在大多数情况下,内置类型之间可以进行转换,例如将短值隐式转换为长值,反之亦然。但对于用户定义类型,编译器需要开发者通过重载转换运算符来明确转换规则。
4.2 转换运算符的实现
转换运算符也需实现为静态公共函数,其语法如下:
- 隐式转换:
public static implicit operator targettype(sourcetype operand)
- 显式转换:
public static explicit operator targettype(sourcetype operand)
以下是一个简单的示例,展示如何将
ZClass
类型转换为
int
类型:
public class ZClass {
public ZClass(int _fielda, int _fieldb) {
fielda = _fielda;
fieldb = _fieldb;
}
public static implicit operator int(ZClass obj) {
return obj.fielda + obj.fieldb;
}
public int fielda, fieldb;
}
class Program {
static void Main() {
ZClass obj = new ZClass(5, 10);
int result = obj; // 隐式转换
Console.WriteLine(result);
}
}
4.3 转换运算符的注意事项
- 转换运算符应确保转换的合理性和直观性,避免出现意外的结果。
- 隐式转换应确保不会丢失数据或引发异常,否则建议使用显式转换。
- 转换运算符的重载也应遵循运算符重载的一般原则,避免过度使用。
5. 运算符重载的实际应用示例
5.1 复数类的运算符重载
假设我们要实现一个复数类
Complex
,并对其进行运算符重载,以支持复数的加法、减法和乘法运算。
using System;
public class Complex {
public double Real { get; set; }
public double Imaginary { get; set; }
public Complex(double real, double imaginary) {
Real = real;
Imaginary = imaginary;
}
// 加法运算符重载
public static Complex operator +(Complex c1, Complex c2) {
return new Complex(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
}
// 减法运算符重载
public static Complex operator -(Complex c1, Complex c2) {
return new Complex(c1.Real - c2.Real, c1.Imaginary - c2.Imaginary);
}
// 乘法运算符重载
public static Complex operator *(Complex c1, Complex c2) {
double real = c1.Real * c2.Real - c1.Imaginary * c2.Imaginary;
double imaginary = c1.Real * c2.Imaginary + c1.Imaginary * c2.Real;
return new Complex(real, imaginary);
}
public override string ToString() {
return $"{Real} + {Imaginary}i";
}
}
class Program {
static void Main() {
Complex c1 = new Complex(3, 4);
Complex c2 = new Complex(1, 2);
Complex sum = c1 + c2;
Complex diff = c1 - c2;
Complex prod = c1 * c2;
Console.WriteLine($"Sum: {sum}");
Console.WriteLine($"Difference: {diff}");
Console.WriteLine($"Product: {prod}");
}
}
5.2 分数类的运算符重载
再来看一个分数类
Fraction
的运算符重载示例,支持分数的加法和减法运算。
public class Fraction {
public int Numerator { get; set; }
public int Denominator { get; set; }
public Fraction(int numerator, int denominator) {
Numerator = numerator;
Denominator = denominator;
}
// 加法运算符重载
public static Fraction operator +(Fraction f1, Fraction f2) {
int numerator = f1.Numerator * f2.Denominator + f2.Numerator * f1.Denominator;
int denominator = f1.Denominator * f2.Denominator;
return new Fraction(numerator, denominator);
}
// 减法运算符重载
public static Fraction operator -(Fraction f1, Fraction f2) {
int numerator = f1.Numerator * f2.Denominator - f2.Numerator * f1.Denominator;
int denominator = f1.Denominator * f2.Denominator;
return new Fraction(numerator, denominator);
}
public override string ToString() {
return $"{Numerator}/{Denominator}";
}
}
class Program {
static void Main() {
Fraction f1 = new Fraction(1, 2);
Fraction f2 = new Fraction(1, 3);
Fraction sum = f1 + f2;
Fraction diff = f1 - f2;
Console.WriteLine($"Sum: {sum}");
Console.WriteLine($"Difference: {diff}");
}
}
6. 运算符重载的总结
6.1 运算符重载的优点
- 增强代码的可读性和直观性:让用户定义类型的使用方式与内置类型相似,使代码更易于理解。
- 提高代码的可维护性:通过合理的运算符重载,可将复杂的操作封装在运算符中,减少代码的冗余。
- 增加代码的灵活性:允许用户根据需要自定义类型的运算规则。
6.2 运算符重载的缺点
- 可能导致代码混淆:如果运算符重载不合理,可能会使代码的含义变得模糊,增加理解难度。
- 增加代码复杂度:过多的运算符重载会使代码变得复杂,不易维护。
- 可移植性问题:由于运算符重载未在所有托管语言中得到支持,可能会影响代码的可移植性。
6.3 使用运算符重载的建议
- 遵循直观性原则:确保运算符重载的行为符合用户的预期,避免产生混淆。
- 适度使用:避免过度使用运算符重载,只在必要时进行重载。
- 实现并行命名方法:为每个运算符重载实现对应的命名方法,提高代码的可移植性。
7. 总结
本文详细介绍了 C# 中的固定大小缓冲区和运算符重载的相关知识。固定大小缓冲区在特定场景下可提供更高效的内存管理,但使用时需遵循严格的规则。运算符重载为用户定义类型提供了更灵活的使用方式,可使代码更加直观和易于理解。但在使用运算符重载时,需遵循相关规则和原则,确保代码的可读性和可维护性。
表格:运算符重载总结
| 运算符类型 | 可重载性 | 注意事项 |
|---|---|---|
| 逻辑和数学运算符(部分) | 可重载 | 遵循语义规则,注意操作数和返回类型 |
| 赋值运算符 | 不可重载 | 可通过单个运算符重载隐式实现 |
| 条件运算符 | 不可重载 | - |
| 增量和减量运算符 | 可重载 | 操作数和返回类型需为包含类或派生类 |
| 左移和右移运算符 | 可重载 |
第一个操作数为包含类类型,第二个为
int
类型
|
| 转换运算符 | 可重载 | 分为隐式和显式转换,确保转换合理性 |
mermaid 流程图:运算符重载使用流程
graph TD;
A[确定是否需要运算符重载] -->|是| B[选择要重载的运算符];
B --> C[遵循运算符重载规则实现函数];
C --> D[测试运算符重载的正确性];
D -->|通过| E[考虑实现并行命名方法];
E --> F[完成运算符重载];
A -->|否| G[使用其他方法实现功能];
通过合理运用固定大小缓冲区和运算符重载,开发者可以更好地利用 C# 语言的特性,提高代码的质量和性能。在实际开发中,应根据具体需求和场景,谨慎选择是否使用这些特性,并遵循相关的规则和原则。
10

被折叠的 条评论
为什么被折叠?



