C#:基本语法看这一篇就够了

前言

本文来自w3c教程的C#教程的学习笔记,对其中的示例有所删减与变更,建议以以下链接为准。

C# 简介_w3cschool

一、C# 与 C++的区别

C# 和 C++ 在语法上有许多相似之处,因为两者都是 C 语言家族的成员,继承了许多 C 语言的语法特性。但它们也有不少差异,主要体现在内存管理、面向对象编程的支持、以及语言设计哲学等方面。有关C/C++文档见以下链接:

C/C++:基本语法看这一篇就够了_h,.n:)!i-CSDN博客

1. 基本语法结构

特性C#C++
程序入口点static void Main(string[] args)int main()
语句结束语句以分号 ; 结束语句以分号 ; 结束
大括号使用大括号 {} 来表示代码块使用大括号 {} 来表示代码块
变量声明int x = 5;int x = 5;

2. 面向对象编程

特性C#C++
类的声明class MyClass { }class MyClass { };
类的继承class Derived : Base { }class Derived : public Base { };
访问修饰符public, private, protected, internalpublic, private, protected
构造函数public MyClass() {}MyClass() {}
析构函数使用 IDisposable 接口来实现资源释放,自动垃圾回收, 以及 ~MyClass() {}~MyClass() {}
接口interface IMyInterface { }没有直接的接口概念,使用抽象类来实现类似接口的功能
属性public int MyProperty { get; set; }没有直接的属性概念,需要通过方法来实现属性功能

3. 内存管理

特性C#C++
内存管理自动垃圾回收 (GC) 管理内存手动内存管理,使用 newdeletemallocfree
指针不直接支持指针,只有通过 unsafe 代码块才支持支持指针,允许直接访问内存
引用支持引用类型,变量和对象引用的赋值是按引用传递的支持引用类型,使用 & 符号声明引用变量

4. 异常处理

特性C#C++
异常处理try, catch, finallytry, catch, throw
异常声明异常不需要在方法声明中声明异常可以声明为 throw() 来表示不抛出异常
自定义异常类可以继承 Exception 类,定义自定义异常类可以继承 std::exception 类,定义自定义异常类

5. 泛型与模板

特性C#C++
泛型使用 generic 类型:List<T>使用模板:template <typename T> class MyClass { };
类型安全泛型类型在运行时类型安全模板类型在编译时类型安全,但没有运行时类型检查

6. 内联函数与委托/回调

特性C#C++
内联函数使用 inline 不常见,更多使用方法封装和事件使用 inline 关键字来提示编译器内联函数
委托与事件委托用于封装方法引用,事件用于发布/订阅模式使用函数指针或 std::function 来实现回调功能

7. 标准库/类库

特性C#C++
标准库.NET 提供丰富的类库,如 System, System.Linq标准库提供多种功能,如 STL(标准模板库)、iostream
字符串类型string 类型为引用类型,自动管理内存std::string 为值类型,需要手动管理内存

8. 多线程与并发

特性C#C++
多线程库使用 System.Threading 来管理线程和并发任务使用 std::thread 来创建线程,并通过 std::mutex 等管理同步
异步编程使用 asyncawait 来简化异步编程C++11 引入了异步编程,但没有专门的关键字,通常使用 std::async

9. 语言特点

特性C#C++
语言设计面向对象为主,强调开发效率和安全性支持面向对象、面向过程和泛型编程,强调对性能的控制
多态实现通过虚方法和接口实现多态通过虚函数和继承实现多态
类型系统强类型语言,类型检查严格强类型语言,类型检查严格

二、开发环境

C# 的开发环境有很多种,Visual Studio、Visual Studio Code等,这里采用Visual Studio。

Visual Studio 是由微软(Microsoft)开发的集成开发环境(IDE),主要用于开发 Windows 和跨平台应用程序,支持多种编程语言,包括 C#、C++、Python、JavaScript、F# 等。它是开发 Windows 应用程序、Web 应用程序、移动应用程序以及游戏等的强大工具之一,具有丰富的功能和广泛的使用场景。

很强,也很复杂,以至于感觉有点难用,所以得讲,避免大家被劝退,这里简单走一下流程。

1、下载与安装

下载 Visual Studio Tools - 免费安装 Windows、Mac、Linux (microsoft.com)

Visual Studio安装可以根据自己需要的组件进行安装,如果还不知道自己需要什么组件,先采用默认安装。后面可以双击安装程序,点击修改,继续组件的安装。

 

2、新建应用

这个是窗体应用,就是cmd那样的窗体,新建完成后会加载窗体环境与配置运行环境,文件很多。如果是一些复杂的应用,加载的东西更多。不同的应用,加载的环境有些区别,有些应用,对于小白来说,不太友好。窗体应用算是比较友好的了。

3、解决方案

Visual Studio 中,"解决方案"(Solution)是一个容器,用来组织一个或多个相关的项目。解决方案本质上是 Visual Studio 中管理和部署项目的方式,它帮助开发者组织代码、资源和依赖项,并控制多个项目之间的关系。

(1)解决方案的基本概念

解决方案(Solution):解决方案是 Visual Studio 中的顶级容器,包含了一个或多个项目。一个解决方案可以包含多个不同类型的项目,例如一个项目是 C# 应用程序,另一个是数据库项目,或者 Web 应用程序等。解决方案通过 .sln 文件来表示,文件名后缀为 .sln,该文件包含了解决方案内各个项目的元数据和设置。

项目(Project):每个解决方案可以包含多个项目。项目是实现特定功能的代码单元,它包含源代码、资源文件、配置文件等内容。每个项目有自己的构建配置和输出目标,如 DLL、EXE 或其他类型的可执行文件。项目通过 .csproj 文件来表示(C# 项目是 .csproj 格式),该文件管理了项目的所有资源和配置。

解决方案资源管理器(Solution Explorer):它是 Visual Studio 的一个工具窗口,用来展示和管理当前打开的解决方案和项目结构。在这个窗口中,你可以查看和操作项目中的文件、引用的库、配置文件等。在这个窗口中,解决方案(Solution)作为最上层的节点展示,下面是项目(Project)和项目中的具体文件、文件夹等内容。

项目类型:项目可以是多种类型的应用程序,例如控制台应用程序(Console App)、Web 应用程序、类库(Library)等。每个项目的目标是实现特定的功能。

(2)对于上述界面

解决方案"ConsoleApp1" (1 个项目/共 1 个):这是一个名为 ConsoleApp1 的解决方案,它包含一个项目,并且该项目的类型是 C# 控制台应用程序。解决方案中当前只有一个项目(ConsoleApp1),并且它包含的代码文件和资源,显示的是 C# 代码。

ConsoleApp1:这是解决方案中的一个项目,表示一个 C# 控制台应用程序。

C#:这个标签说明该项目是一个 C# 类型的项目,即使用 C# 语言编写的。

PropertiesProperties 是每个 C# 项目中都有的文件夹,包含项目的设置和配置文件,如 AssemblyInfo.cs 文件,它定义了程序集的元数据。

引用 (References)References 文件夹包含了项目依赖的所有外部库(如 DLL 文件)和其他项目。通过引用,项目可以使用外部库或其他项目中的代码。

App.config:这是 C# 项目中的配置文件,用于存储应用程序的配置信息,例如数据库连接字符串、应用程序设置等。

Program.csProgram.cs 是 C# 控制台应用程序的默认入口点。这个文件包含了 Main 方法,这是应用程序执行的起始点。

4、入口文件

这个界面就是我们编程的界面,把程序编入static void Main(string[] args)函数里面即可。

5、加载库

右键方案》添加》引用:浏览库文件添加。

6、运行

三、语法基础

1、数据类型

类型类别类型示例描述范围/默认值使用方式/示例代码
值类型 (Value Types)bool, byte, char, decimal, double, float, int, long, sbyte, short, uint, ulong, ushort值类型直接包含数据,存储在栈上。每次赋值都会复制值。bool: False, int: 0, char: '', float: 0.0F, double: 0.0D 等int a = 10;
float f = 3.14F;
char c = 'A';
引用类型 (Reference Types)object, dynamic, string, class, interface, delegate引用类型存储的是内存地址而非数据本身,指向堆上的对象。多个变量可以引用相同的内存位置。由具体对象决定,如 string 默认空字符串 ""object 默认 nullstring str = "Hello";
object obj = 100;
dynamic d = 10.5;
指针类型 (Pointer Types)char*, int*, float*, 等指针类型存储内存地址,允许直接操作内存。只能在 unsafe 代码块中使用。在 C# 中需在 unsafe 环境下使用,无默认值unsafe { int* ptr = &a; }

(1)指针使用方式示例

// 指针使用示例,还需要设置Visual Code,让其运行不安全代码
unsafe
{
                int a = 10;
                int* ptr = &a;  // 获取 a 的内存地址
                Console.WriteLine(*ptr);  // 通过指针访问 a 的值
                Console.ReadLine();
}

 2、类型转换

转换类型描述示例代码
隐式类型转换 (Implicit Conversion)C# 默认的类型转换方式,编译器会自动进行转换,通常是从较小类型到较大类型,或从派生类到基类。int i = 10;
long l = i; // int 转换为 long
显式类型转换 (Explicit Conversion)用户通过强制转换运算符显式地进行转换,通常用于不同类型之间的转换,需要注意类型兼容性。double d = 5673.74;
int i = (int)d; // 强制转换为 int
ToBoolean将类型转换为布尔型。bool b = Convert.ToBoolean("true");
ToByte将类型转换为字节类型。byte b = Convert.ToByte(123.45);
ToChar将类型转换为单个 Unicode 字符类型。char c = Convert.ToChar(65); // ASCII 'A'
ToDateTime将类型(整数或字符串类型)转换为日期时间类型。DateTime dt = Convert.ToDateTime("2021-12-31");
ToDecimal将浮点型或整数类型转换为十进制类型。decimal dec = Convert.ToDecimal(123.45);
ToDouble将类型转换为双精度浮点型。double d = Convert.ToDouble(123);
ToInt16将类型转换为 16 位整数类型。short s = Convert.ToInt16(123);
ToInt32将类型转换为 32 位整数类型。int i = Convert.ToInt32(123.45);
ToInt64将类型转换为 64 位整数类型。long l = Convert.ToInt64(123.45);
ToSByte将类型转换为有符号字节类型。sbyte sb = Convert.ToSByte(127);
ToSingle将类型转换为单精度浮点数类型。float f = Convert.ToSingle(123.45);
ToString将类型转换为字符串类型。string str = Convert.ToString(123);
ToType将类型转换为指定的目标类型。object obj = 123;
int i = (int)obj;
ToUInt16将类型转换为 16 位无符号整数类型。ushort us = Convert.ToUInt16(123);
ToUInt32将类型转换为 32 位无符号整数类型。uint ui = Convert.ToUInt32(123.45);
ToUInt64将类型转换为 64 位无符号整数类型。ulong ul = Convert.ToUInt64(123.45);

 3、变量

概念/类型描述举例
变量定义在 C# 中,变量是供程序操作的存储区,必须声明其数据类型。int i, j, k;
数据类型每个变量都有一个特定的类型,决定了内存大小和布局。int, double, char, float
变量初始化变量可以在声明时进行初始化,通过赋值给变量。int i = 100;, double pi = 3.14;
整数类型用于存储整数值。包括有符号和无符号整数类型。int, long, byte, sbyte, char
浮点类型用于存储小数(浮点数)值。float, double
十进制类型用于高精度存储十进制数,通常用于财务和货币计算。decimal
布尔类型用于存储逻辑值,通常表示真(true)或假(false)。bool
空类型用于表示可能为空的值。Nullable<int>, int?
Lvalue左值(Lvalue):可以出现在赋值语句的左边,表示可修改的内存位置。int g = 20; (g 是 Lvalue)
Rvalue右值(Rvalue):可以出现在赋值语句的右边,表示常量或表达式的结果。20 = 20;(无效,因为 20 是 Rvalue)

 4、常量

概念描述示例
常量常量是固定值,在程序执行期间不能改变。常量可以是任何基本数据类型,如整数、浮点数、字符、字符串等。const int x = 10;
整数常量整数常量可以是十进制、八进制、十六进制,带有后缀的无符号和长整型常量。85, 0213, 0x4b, 30u, 30l
浮点常量浮点常量由整数部分、小数部分和指数部分组成,支持小数形式和指数形式。3.14159, 314159E-5L, 510E, 210f
字符常量字符常量用单引号括起来,可以是普通字符、转义序列或通用字符。'x', '\t', '\n', '\u02C0'
字符串常量字符串常量用双引号 "" 括起来,也可以使用 @"" 形式表示原始字符串。"hello, dear", "hello, \ndear", @"hello dear"
转义序列用于表示特殊字符,如换行符、制表符等。常用于字符常量和字符串常量中。\n, \t, \\, \u02C0
常量声明常量通过 const 关键字声明,声明后其值不可更改。const double pi = 3.14159;
常量实例示例代码演示常量的定义和使用。通过常量计算圆的面积。const double pi = 3.14159; double area = pi * r * r;

 5、运算符

这里是对 C# 中运算符的总结,包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符以及杂项运算符:

(1)算术运算符

运算符

描述

实例

+

把两个操作数相加

A + B 将得到 30

-

从第一个操作数中减去第二个操作数

A - B 将得到 -10

*

把两个操作数相乘

A * B 将得到 200

/

分子除以分母

B / A 将得到 2

%

取模运算符,整除后的余数

B % A 将得到 0

++

自增运算符,整数值增加 1

A++ 将得到 11

--

自减运算符,整数值减少 1

A-- 将得到 9

(2)关系运算符

运算符

描述

实例

==

检查两个操作数的值是否相等

(A == B) 不为真

!=

检查两个操作数的值是否不相等

(A != B) 为真

>

检查左操作数的值是否大于右操作数的值

(A > B) 不为真

<

检查左操作数的值是否小于右操作数的值

(A < B) 为真

>=

检查左操作数的值是否大于或等于右操作数

(A >= B) 不为真

<=

检查左操作数的值是否小于或等于右操作数

(A <= B) 为真

(3)逻辑运算符

运算符

描述

实例

&&

逻辑与运算符,如果两个操作数都为真则结果为真

(A && B) 为假

`

`

!

逻辑非运算符,用来逆转操作数的逻辑状态

!(A && B) 为真

(4)位运算符

运算符

描述

实例

&

位与运算符,如果同时存在于两个操作数中

(A & B) 将得到 12

`

`

位或运算符,如果存在于任一操作数中

^

位异或运算符,如果存在于其中一个操作数中但不同时存在

(A ^ B) 将得到 49

~

位补码运算符,翻转位效果

(~A) 将得到 -61

<<

位左移运算符,将左操作数的值左移指定的位数

(A << 2) 将得到 240

>>

位右移运算符,将左操作数的值右移指定的位数

(A >> 2) 将得到 15

(5)赋值运算符

运算符

描述

实例

=

简单的赋值运算符,把右边操作数的值赋给左边操作数

C = A + B 将把 A + B 的值赋给 C

+=

加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数

C += A 相当于 C = C + A

-=

减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数

C -= A 相当于 C = C - A

*=

乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数

C *= A 相当于 C = C * A

/=

除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数

C /= A 相当于 C = C / A

%=

求模且赋值运算符,求两个操作数的模赋值给左边操作数

C %= A 相当于 C = C % A

<<=

左移且赋值运算符

C <<= 2 等同于 C = C << 2

>>=

右移且赋值运算符

C >>= 2 等同于 C = C >> 2

&=

按位与且赋值运算符

C &= 2 等同于 C = C & 2

^=

按位异或且赋值运算符

C ^= 2 等同于 C = C ^ 2

`

=`

按位或且赋值运算符

(6)杂项运算符

运算符

描述

实例

sizeof()

返回数据类型的大小

sizeof(int) 将返回 4

typeof()

返回类的类型

typeof(StreamReader)

&

返回变量的地址

&a 将得到变量的实际地址

*

变量的指针

*a 将指向一个变量

? :

条件表达式

(a == 1) ? 20 : 30

is

判断对象是否为某一类型

Ford is Car 检查 Ford 是否是 Car 类型的对象

as

强制转换,即使转换失败也不会抛出异常

obj as StringReader


(7)运算符优先级

C# 运算符的优先级决定了在一个表达式中运算符的执行顺序。高优先级的运算符会先执行。以下是按优先级从高到低排列的运算符:

优先级

运算符

结合性

1

() [] -> . ++ --

从左到右

2

+ - ! ~ ++ -- (type) * & sizeof

从右到左

3

* / %

从左到右

4

+ -

从左到右

...

...

...

此优先级列表会影响运算顺序,在没有括号的情况下,具有高优先级的运算符会首先被计算。

6、 判断

语句类型描述示例
if 语句由一个布尔表达式后跟一个或多个语句组成,执行条件为真时的语句。if (a >= 60) { Console.WriteLine("及格"); }
if...else 语句一个 if 语句后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行。if (a >= 60) { Console.WriteLine("及格"); } else { Console.WriteLine("不及格"); }
嵌套 if 语句在一个 if 或 else if 语句内使用另一个 if 或 else if 语句。if (a > 90) { Console.WriteLine("优秀"); } else if (a >= 60) { Console.WriteLine("及格"); }
switch 语句允许测试一个变量等于多个值时的情况,通常用于多重条件判断。switch (a) { case 1: Console.WriteLine("一"); break; case 2: Console.WriteLine("二"); break; }
嵌套 switch 语句在一个 switch 语句内使用另一个 switch 语句,处理复杂的多重条件。switch (a) { case 1: switch (b) { case 1: Console.WriteLine("一"); break; } break; }
三目运算符 (? :)通过 Exp1 ? Exp2 : Exp3 表达式替代 if...else 语句,Exp1 为真时返回 Exp2,否则返回 Exp3。result = (a >= 60) ? "及格" : "不及格";
条件运算符替代 if? : 运算符只能用于表达式,不能执行函数操作,但适用于简单的判断逻辑,常用于返回值的判断。a >= 60 ? true : false;

 7、循环

(1)循环类型

循环类型描述示例
while 循环当给定条件为真时,重复执行语句或语句组。在执行循环主体之前会测试条件。while (a < 10) { Console.WriteLine(a); a++; }
for 循环多次执行一个语句序列,简化循环变量的管理。通常用于已知循环次数的情况。for (int i = 0; i < 10; i++) { Console.WriteLine(i); }
foreach 循环用于遍历集合类型(如数组、列表等),简化了循环访问集合元素的方式。foreach (var item in array) { Console.WriteLine(item); }
do...while 循环在循环体结束后测试条件,保证循环体至少执行一次。do { Console.WriteLine(a); a++; } while (a < 10);
嵌套循环在一个循环内部再嵌套另一个或多个循环,常用于处理多维数据。for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { Console.WriteLine(i + " " + j); } }

(2)循环控制

控制语句描述示例
break终止当前循环或 switch 语句,程序流会继续执行 loopswitch 后的下一条语句。while (true) { if (a == 10) break; Console.WriteLine(a); a++; }
continue跳过循环主体的剩余部分,立即回到循环条件的判断阶段,继续执行下一个循环。for (int i = 0; i < 10; i++) { if (i % 2 == 0) continue; Console.WriteLine(i); }
无限循环如果条件永远为真,循环将持续无限执行。通常可以使用 for(;;) 结构实现。for (; ;) { Console.WriteLine("Hey! I am Trapped"); }

8、封装

修饰符描述访问范围示例
Public公开访问修饰符,允许类的成员被所有类和对象访问。任何类或对象都可以访问。public int x;
Private私有访问修饰符,仅允许类内部的成员函数访问该成员。其他类或对象无法访问。只能在定义该成员的类内部访问。private int x;
Protected保护访问修饰符,允许子类访问基类的成员,但类外的其他对象无法访问。当前类及其派生类可以访问。protected int x;
Internal内部访问修饰符,允许同一个程序集内的其他类或对象访问。同一程序集内的类和对象可以访问。internal int x;
Protected internal受保护内部访问修饰符,允许同一程序集内的类、派生类及其成员访问。同一程序集内的类、派生类以及当前类可以访问。protected internal int x;

 (1)Public示例

using System;

namespace RectangleApplication
{
    class Rectangle
    {
        public double length;   // 公共成员变量
        public double width;

        public double GetArea()  // 公共方法
        {
            return length * width;
        }

        public void Display()
        {
            Console.WriteLine("Length: " + length);
            Console.WriteLine("Width: " + width);
            Console.WriteLine("Area: " + GetArea());
        }
    }

    class ExecuteRectangle
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle();
            r.length = 5;   // 直接访问公有成员
            r.width = 10;
            r.Display();
        }
    }
}

9、方法

概念描述示例代码
方法定义定义方法的语法:<Access Specifier> <Return Type> <Method Name>(Parameter List) { Method Body }public int FindMax(int num1, int num2) { return (num1 > num2) ? num1 : num2; }
方法调用使用方法名调用方法,方法可以带参数。ret = n.FindMax(a, b); Console.WriteLine("最大值是: {0}", ret);
递归方法调用方法可以自我调用,这被称为递归。public int factorial(int num) { if (num == 1) return 1; else return factorial(num - 1) * num; }
值参数传递传递的是值的副本。方法修改参数时,不会影响实际参数的值。public void swap(int x, int y) { int temp = x; x = y; y = temp; }
n.swap(a, b); // a 和 b 的值不变
引用参数传递 (ref)传递的是参数的引用,方法可以修改参数的值,影响实际参数。public void swap(ref int x, ref int y) { int temp = x; x = y; y = temp; }
n.swap(ref a, ref b); // a 和 b 的值会交换
输出参数传递 (out)用于从方法中返回多个值,参数无需初始化。在方法内赋值后,调用者获取新的值。public void getValue(out int x) { x = 5; }
n.getValue(out a); // a 被赋值为 5
方法返回值方法通过 return 语句返回一个值。public int Add(int x, int y) { return x + y; }
实例:交换值(值传递)演示了值传递时,交换操作对原始值没有影响。Console.WriteLine("交换前: a={0}, b={1}", a, b);
n.swap(a, b);
Console.WriteLine("交换后: a={0}, b={1}", a, b); // 结果显示 a 和 b 值没有变化
实例:交换值(引用传递)演示了引用传递时,交换操作会影响原始值。Console.WriteLine("交换前: a={0}, b={1}", a, b);
n.swap(ref a, ref b);
Console.WriteLine("交换后: a={0}, b={1}", a, b); // 结果显示 a 和 b 值交换
实例:获取多个值演示了输出参数可以从方法中返回多个值。public void getValues(out int x, out int y) { x = 7; y = 8; }
n.getValues(out a, out b);
Console.WriteLine("a={0}, b={1}", a, b);

10、可空类型

概念描述语法与示例结果示例
可空类型(Nullable Types)可空类型允许值类型(如 intbooldouble)可以接受一个 null 值,表示该值未定义。<data_type>? <variable_name> = null;int? num1 = null;
int? num2 = 45;
Console.WriteLine(num1, num2);
输出null, 45
可空类型声明? 声明可空类型,允许为该类型赋值 null 或者其正常值范围内的数值。int? num1 = null;
double? num3 = new double?();
num1 = null
num3 = null
空值输出可空类型变量若未赋值,输出为空值 nullbool? boolval = new bool?();
Console.WriteLine(boolval);
输出null
空值合并运算符(??)?? 运算符用于提供一个默认值。如果可空类型的值为 null,则返回指定的默认值,否则返回可空类型的值。num3 = num1 ?? 5.34;
num3 = num2 ?? 5.34;
输出num3 = 5.34(当 num1null 时使用默认值 5.34)
num3 = 3.14157(当 num2 不为 null 时使用 num2 值)

 11、数组

概念描述语法与示例结果示例
数组声明数组是一个固定大小的顺序集合,用来存储相同类型的元素。数组变量可以通过索引访问。datatype[] arrayName;
double[] balance;
声明一个 double[] 类型的数组变量 balance
数组初始化声明数组后需要使用 new 关键字初始化数组,并指定数组的大小或赋初值。double[] balance = new double[10];
int[] marks = { 99, 98, 92, 97, 95 };
初始化数组 balance,大小为 10。
初始化数组 marks,并赋值 { 99, 98, 92, 97, 95 }
赋值给数组使用索引为数组中的特定元素赋值。可以在声明数组时同时初始化数组元素。balance[0] = 4500.0;
int[] marks = new int[5] { 99, 98, 92, 97, 95 };
赋值 balance[0] = 4500.0; 和初始化 marks 数组。
访问数组元素数组元素通过索引来访问。索引从 0 开始。数组元素通过 array[index] 的方式访问。double salary = balance[9];访问 balance[9] 元素的值并赋值给 salary
使用 foreach 循环使用 foreach 循环遍历数组元素。比 for 循环更简洁。foreach (int j in n) { Console.WriteLine("Element[{0}] = {1}", i, j); }输出数组 n 中的每个元素值。
多维数组C# 支持多维数组,最简单的是二维数组。多维数组中的元素通过多个索引访问。int[,] matrix = new int[3, 4];声明并初始化一个 3 行 4 列的二维数组 matrix
交错数组C# 支持交错数组,即数组的数组,类似于二维数组但每行的长度不固定。int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[2];
声明一个交错数组 jaggedArray,其中每个子数组的长度可以不同。
传递数组给函数数组可以作为参数传递给函数,传递的是数组的引用。void DisplayArray(int[] arr) { }
DisplayArray(marks);
marks 数组传递给 DisplayArray 函数。
参数数组通过 params 关键字传递未知数量的参数,允许传递不同长度的参数数组。void DisplayNumbers(params int[] numbers) { }使用 params 传递多个数值给函数 DisplayNumbers
Array 类Array 类是所有数组的基类,提供了数组的各种方法和属性。Array.Sort(array);
Array.Length
使用 Array.Sort() 对数组进行排序,使用 array.Length 获取数组的长度。

12、 字符串

概念/方法描述示例代码
创建 String 对象创建 String 对象的几种方式:
1. 通过给 String 变量指定一个字符串直接给变量赋值字符串。string fname = "Rowan";
2. 通过使用 String 类构造函数使用字符数组构造一个新的 String 对象。char[] letters = { 'H', 'e', 'l', 'l', 'o' }; string greetings = new string(letters);
3. 通过字符串串联运算符(+)使用 + 运算符将字符串连接成一个新的 String 对象。string fullname = fname + lname;
4. 通过方法返回字符串调用方法并返回字符串。string message = String.Join(" ", sarray);
5. 通过格式化方法使用 String.Format 方法将值转换为字符串表示形式。string chat = String.Format("Message sent at {0:t} on {0:D}", waiting);
String 类的属性String 类的常用属性。
1. Chars获取当前 String 对象中指定位置的字符。char ch = myString[0];
2. Length获取 String 对象中的字符数。int length = myString.Length;
String 类的常用方法String 类的常用方法。
1. Compare比较两个字符串并返回它们在排序顺序中的位置。int result = String.Compare(str1, str2);
2. Concat连接两个或多个字符串。string fullName = String.Concat(firstName, lastName);
3. Contains判断当前字符串是否包含指定的子字符串。bool contains = str.Contains("test");
4. Copy创建一个新的 String 对象,值与指定字符串相同。string newStr = String.Copy(str);
5. CopyTo从指定位置开始将当前字符串的字符复制到字符数组。myString.CopyTo(0, destArray, 0, 5);
6. EndsWith判断当前字符串是否以指定的子字符串结尾。bool endsWith = str.EndsWith("test");
7. Equals判断两个字符串是否相等。bool equals = str1.Equals(str2);
8. Format格式化字符串,将指定的格式项替换为对象的字符串表示。string formattedStr = String.Format("Price: {0:C}", price);
9. IndexOf返回指定字符或子字符串第一次出现的索引。int index = str.IndexOf("test");
10. Insert在指定索引位置插入字符串。string result = str.Insert(5, "ABC");
11. IsNullOrEmpty判断字符串是否为 null 或空字符串。bool isEmpty = String.IsNullOrEmpty(str);
12. Join将字符串数组中的所有元素连接成一个单一字符串,并用指定分隔符分隔。string result = String.Join(", ", array);
13. LastIndexOf返回指定字符或子字符串最后一次出现的索引。int lastIndex = str.LastIndexOf("test");
14. Remove从指定位置开始移除字符。string result = str.Remove(5);
15. Replace替换指定字符或子字符串。string result = str.Replace("old", "new");
16. Split按指定字符分割字符串并返回子字符串数组。string[] substrings = str.Split(',');
17. StartsWith判断字符串是否以指定的子字符串开始。bool startsWith = str.StartsWith("test");
18. ToCharArray将字符串转换为字符数组。char[] charArray = str.ToCharArray();
19. ToLower将字符串转换为小写。string lowerStr = str.ToLower();
20. ToUpper将字符串转换为大写。string upperStr = str.ToUpper();
21. Trim移除字符串的前导和尾随空白字符。string trimmedStr = str.Trim();

13、 结构

序号主题描述
1定义结构使用 struct 关键字定义结构,例如 struct Books
2结构成员结构可以包含字段、方法、属性、索引器、事件等。
3构造函数结构可以定义构造函数,但不能定义析构函数。结构没有默认构造函数。
4值类型与引用类型结构是值类型,赋值给另一个结构时会复制其数据,而类是引用类型。
5结构的继承结构不能继承类或其他结构,也不能作为其他结构或类的基类。
6接口实现结构可以实现一个或多个接口。
7成员权限结构成员不能指定为 abstractvirtualprotected
8默认实例化结构可以通过 new 运算符创建实例,也可以在未使用 new 的情况下实例化。
9字段初始化当不使用 new 时,结构的字段只有在初始化后才能使用。
10结构与类的对比结构和类的主要不同点包括:结构是值类型,结构不支持继承,结构不能声明默认构造函数等。

 (1)对比类与结构

特性结构(Struct)类(Class)
类型值类型引用类型
继承不支持继承支持继承
默认构造函数自动提供但不可更改可以定义自定义构造函数
字段初始化可以不使用 new 关键字实例化必须使用 new 关键字实例化
大小存储在栈上(一般较小)存储在堆上(一般较大)
垃圾回收不受垃圾回收控制由垃圾回收器管理

 14、枚举

序号主题描述
1定义枚举使用 enum 关键字声明枚举类型,例如:enum Days { Sun, Mon, tue, Wed, thu, Fri, Sat };
2枚举的值枚举成员的默认值从 0 开始,每个后续成员的值比前一个大 1。可以通过显式指定值来改变默认值。
3枚举成员的值每个枚举成员实际上是一个整数值,默认从 0 开始。如果需要,可以手动赋予枚举成员不同的整数值。
4枚举类型枚举是值类型,它具有自己的数据值,并且不能被继承或传递继承。
5枚举与整数转换枚举成员可以通过显式转换为整数值,整数也可以被转换回对应的枚举类型。

15、 类

概念描述示例代码
类的定义使用 class 关键字定义类,类的成员包括变量和方法。class Box { public double length; public double breadth; public double height; }
实例化类使用 new 关键字创建类的实例(对象)。Box Box1 = new Box(); Box Box2 = new Box();
成员变量类的属性,定义类的状态。public double length;
成员方法类的行为或操作,用于定义类的功能。public double getVolume() { return length * breadth * height; }
封装通过访问控制符将类的成员限制访问,常将变量设为 private,并通过 public 方法访问。private double length;
public void setLength(double len) { length = len; }
构造函数特殊方法,用于在创建对象时初始化对象的成员。构造函数名称与类名相同。可以是默认构造函数或参数化构造函数。public Line() { Console.WriteLine("对象已创建"); }
public Line(double len) { length = len; }
析构函数特殊方法,用于对象超出作用域时释放资源。析构函数的名称是类名加波浪符(~)。~Line() { Console.WriteLine("对象已删除"); }
静态成员属于类而非实例的成员,所有实例共享一个副本。静态成员使用 static 关键字。public static int num;
public static int getNum() { return num; }
静态方法静态方法只能访问静态变量,不能访问实例成员。public static int getNum() { return num; }
访问控制符控制类成员的访问权限,常见的有 publicprivateprotectedinternalpublic double length;
private double length;
成员的访问使用点运算符 (.) 来访问对象的成员。Box1.length = 5.0;
Console.WriteLine(Box1.getVolume());
类与对象的关系类是一个模板,定义了对象的结构和行为,对象是类的实例。Box Box1 = new Box();

16、继承

概念描述示例代码
继承概述继承是面向对象程序设计中的重要概念,允许一个类继承另一个类的成员,使得代码复用和维护变得更容易。派生类继承了基类的成员,可以重用基类的代码。
基类与派生类基类是被继承的类,派生类是继承基类的类。派生类可访问基类的公有和保护成员,不能访问基类的私有成员。基类 Shape 和派生类 Rectangle
属于(IS-A)关系继承实现了“属于”关系。例如,狗属于哺乳动物哺乳动物属于动物,所以狗属于动物Shape 是一个基类,Rectangle 是派生类。
基类的成员基类的成员(变量和方法)会被派生类继承,派生类可以重用基类的代码并扩展功能。基类成员:public void setWidth(int w)public void setHeight(int h)
构造函数与初始化派生类无法继承基类的构造函数,但可以通过构造函数初始化基类成员。派生类对象创建前,基类对象先被创建。public Rectangle(double l, double w) : base(l, w)
代码示例:继承实现创建基类和派生类,派生类扩展了基类的功能。class Shape { ... }
class Rectangle : Shape { ... }
多重继承C# 不支持多重继承,但可以通过接口来实现类似的功能。使用接口 PaintCost 实现多重继承:class Rectangle : Shape, PaintCost { ... }
多重继承示例通过接口模拟多重继承,实现多个类的功能组合。public interface PaintCost { int getCost(int area); }
class Rectangle : Shape, PaintCost { ... }
派生类扩展功能派生类不仅继承基类的功能,还可以添加更多成员或覆盖基类的方法来扩展功能。Rectangle 中添加方法 getArea()getCost(),并通过 Display() 输出数据。
基类初始化与成员基类在派生类的构造函数中进行初始化,派生类在初始化时获得基类的成员。public Tabletop(double l, double w) : base(l, w)
base.Display()

17、多态性

概念描述示例代码
多态性概述多态性意味着有多重形式,它允许通过同一接口调用不同的功能。在面向对象编程中,多态性通常表现为"一个接口,多个功能"。- 静态多态性:函数重载和运算符重载。
- 动态多态性:通过抽象类和虚方法来实现运行时的多态。
静态多态性静态多态性(也称为早期绑定)是通过编译时确定函数的响应。静态多态性通过函数重载和运算符重载实现。- 函数重载示例:同一个函数名根据参数类型或数量的不同执行不同的功能。
函数重载函数重载是指在同一个类中,允许有多个相同函数名,但它们的参数列表不同(如类型不同或个数不同)。

void print(int i) { ... }

void print(double f) { ... }

void print(string s) { ... }

动态多态性动态多态性(也称为晚期绑定)是指在运行时根据对象的实际类型来决定调用哪个方法。通过抽象类和虚方法来实现。- 使用抽象类和虚方法实现不同派生类的不同实现。例如,基类定义一个虚方法,派生类覆盖它。
抽象类与抽象方法抽象类不能被实例化,它包含抽象方法,派生类必须重写这些方法。抽象方法在基类中没有实现,而是由派生类来实现。

abstract class Shape { public abstract int area(); }

class Rectangle : Shape { public override int area() { ... } }

虚方法虚方法有默认实现,派生类可以选择是否重写它。虚方法支持运行时多态,允许基类与派生类的行为不同。

class Shape { public virtual int area() { ... } }

class Rectangle : Shape { public override int area() { ... } }

抽象方法与虚方法区别- 抽象方法没有实现,必须在派生类中实现,派生类不能实例化。
- 虚方法有默认实现,可以在派生类中覆盖,也可以不覆盖。
- 抽象方法:必须在派生类中重写,且派生类必须是抽象类。
- 虚方法:可以选择覆盖或不覆盖。

 18、运算符重载

运算符描述是否可以重载
+二元运算符,用于执行加法操作。可重载
-二元运算符,用于执行减法操作。可重载
*二元运算符,用于执行乘法操作。可重载
/二元运算符,用于执行除法操作。可重载
%二元运算符,用于执行求余操作。可重载
!一元运算符,用于取反操作。可重载
~一元运算符,用于按位取反操作。可重载
++一元运算符,用于执行自增操作。可重载
--一元运算符,用于执行自减操作。可重载
==比较运算符,用于检查两个对象是否相等。可重载
!=比较运算符,用于检查两个对象是否不相等。可重载
<比较运算符,用于检查左边的对象是否小于右边的对象。可重载
>比较运算符,用于检查左边的对象是否大于右边的对象。可重载
<=比较运算符,用于检查左边的对象是否小于或等于右边的对象。可重载
>=比较运算符,用于检查左边的对象是否大于或等于右边的对象。可重载
&&逻辑运算符,用于执行逻辑与操作。不可重载
+=赋值运算符,用于执行加法赋值操作。不可重载
-=赋值运算符,用于执行减法赋值操作。不可重载
*=赋值运算符,用于执行乘法赋值操作。不可重载
/=赋值运算符,用于执行除法赋值操作。不可重载
=赋值运算符,用于执行简单赋值操作。不可重载
.用于访问对象的成员(字段、方法等)。不可重载
?:三元运算符,用于根据条件表达式返回不同的值。不可重载
->用于访问结构体的成员。不可重载
new用于创建对象实例。不可重载
is用于检查对象是否为指定类型。不可重载
sizeof用于获取数据类型的大小。不可重载
typeof用于获取类型的实例。不可重载

(1)可重载示例

// 在下面的例子中,我们重载了 + 运算符,使得 Box 类的对象可以通过加法运算符相加。
using System;

namespace OperatorOverloadExample
{
    class Box
    {
        public double Length { get; set; }
        public double Width { get; set; }
        public double Height { get; set; }

        // 重载 + 运算符
        public static Box operator +(Box b1, Box b2)
        {
            Box result = new Box();
            result.Length = b1.Length + b2.Length;
            result.Width = b1.Width + b2.Width;
            result.Height = b1.Height + b2.Height;
            return result;
        }

        public override string ToString()
        {
            return $"Length: {Length}, Width: {Width}, Height: {Height}";
        }
    }

    class Program
    {
        static void Main()
        {
            Box box1 = new Box { Length = 5, Width = 3, Height = 2 };
            Box box2 = new Box { Length = 4, Width = 2, Height = 1 };
            Box box3 = box1 + box2;  // 使用重载的 + 运算符

            Console.WriteLine("Box1: " + box1);
            Console.WriteLine("Box2: " + box2);
            Console.WriteLine("Box3 (Box1 + Box2): " + box3);
        }
    }
}

(2)不可重载示例

// C# 中的逻辑运算符,如 && 和 || 是不可重载的,不能自定义它们的行为。以下是一个试图重载 && 运算符的例子,它会导致编译错误。

using System;

namespace OperatorOverloadExample
{
    class Box
    {
        public double Length { get; set; }

        // 编译时错误:无法重载 && 运算符
        public static bool operator &&(Box b1, Box b2)
        {
            return b1.Length > 5 && b2.Length > 5;
        }
    }

    class Program
    {
        static void Main()
        {
            Box box1 = new Box { Length = 6 };
            Box box2 = new Box { Length = 7 };
            bool result = box1 && box2;  // 编译错误
            Console.WriteLine(result);
        }
    }
}

19、接口

特性接口抽象类
定义方式使用 interface 关键字声明使用 abstract class 关键字声明
成员定义只包含成员声明,没有实现。成员可以是方法、属性、事件。包含方法声明和实现。可以包含字段、属性、构造函数等。
构造函数不能包含构造函数可以包含构造函数
字段不能包含字段可以包含字段
访问修饰符成员默认是 public,不能使用 privateprotected 等修饰符可以使用不同的访问修饰符,如 privateprotectedpublic
成员实现要求实现接口时,必须实现接口中声明的所有方法和属性只需要实现抽象方法,其他方法可以有实现。
多重继承支持多重继承,一个类可以实现多个接口不支持多重继承,只能继承一个抽象类或其他类
继承对象类继承接口时,必须实现接口中的所有方法类继承抽象类时,必须实现抽象方法
适用场景用于定义类的契约或协议,类通过接口规范行为用于定义类的通用行为并提供部分实现

 20、命名空间

特性描述
定义使用 namespace 关键字定义,后跟命名空间的名称。例如:namespace namespace_name { }
作用提供一种机制将一组相关名称(如类、函数)分组,以避免与其他命名空间中的同名元素冲突。
访问命名空间中的成员通过将命名空间名称放在前面访问成员,例如:namespace_name.item_name
嵌套命名空间一个命名空间可以包含另一个命名空间,使用点(.)运算符访问嵌套命名空间中的成员。例如:first_space.second_space.member_name
using 关键字using 关键字允许在程序中使用命名空间中的成员时,不需要每次都写出完整的命名空间路径。例如:using first_space;
完全限定名称如果没有使用 using 关键字,可以使用完全限定名称访问命名空间中的成员,例如:System.Console.WriteLine("Hello");
访问多个命名空间可以使用多个 using 语句引入不同的命名空间。例如:using first_space; using second_space;
成员访问直接访问成员时,使用命名空间作为前缀,如:first_space.namespace_cl。通过实例化类后,调用其方法:fc.func()
命名空间成员的限制命名空间不包含实现,只有类、结构、接口和枚举等成员的声明。实现由类或其他类型负责。

21、预处理指令

预处理器指令描述
#define定义一个符号常量,后续可在条件编译中使用。例如,#define PI 定义一个符号 PI
#undef取消已定义的符号。例如,#undef PI 取消对符号 PI 的定义。
#if用于测试一个符号是否为真,条件成立时编译器编译 #if 和下一个指令之间的代码。例如,#if DEBUG 测试 DEBUG 是否已定义。
#else#if 配合使用,在条件不成立时执行的代码部分。例如,#else 语句会在 #if 条件未满足时执行。
#elif#if 配合使用,表示如果 #if 条件不满足时,使用新的条件进行判断。例如,#elif DEBUG
#endif结束一个条件编译块。必须在 #if#elif 语句之后使用。
#line修改编译器的行号,或指定输出错误和警告时的文件名。例如,#line 100 可以修改当前行号为 100。
#error生成一个编译时错误,允许开发者在代码中指定错误。
#warning生成一个编译时警告,允许开发者在代码中指定警告。
#region在 Visual Studio 中指定一个可折叠的代码区域,使代码更易于管理和查看。
#endregion结束一个 #region 块,标识该代码区域的结束。

22、正则表达式

类别描述示例模式匹配
字符转义在正则表达式中使用反斜杠来转义字符。\a, \b, \t匹配警告符、退格符、制表符等特定字符。
字符类用于匹配字符集中的任意一个字符。[a-z]匹配任何小写字母。
定位点零宽度断言,指示匹配应位于特定位置。^, $匹配字符串的开头或结尾。
分组构造用来创建子表达式,通常用于捕获子字符串。(abc)匹配 "abc" 并捕获该部分。
限定符用于指定一个元素的出现次数。*, +, ?匹配字符零次或多次,至少一次,或零次一次。
反向引用构造用于引用之前捕获的子表达式。\1, \k<name>匹配捕获组的内容。
备用构造用于启用“或”条件匹配。`ab`
替换替换匹配到的字符串。$1, $&替换捕获组内容或整个匹配内容。
杂项构造其他类型的构造,包括注释、模式选项等。(?x)在模式中启用或禁用选项。
Regex 类方法用于在代码中实现正则表达式匹配、替换、分割等操作。IsMatch(), Replace()检查字符串匹配、替换字符串、分割字符串等操作。

23、异常处理

(1)处理机制

关键词描述
try标识一段可能抛出异常的代码。后跟一个或多个 catch 块。
catch捕获异常并处理,通常会使用异常类型作为条件。
finally无论是否发生异常,都会执行的代码块,通常用于清理资源。
throw用于显式抛出异常。可以在 catch 块内重新抛出当前异常。

(2)异常类

异常类描述
System.IO.IOException处理 I/O 错误。
System.IndexOutOfRangeException处理数组索引越界错误。
System.ArrayTypeMismatchException处理数组类型不匹配错误。
System.NullReferenceException处理空对象引用错误。
System.DivideByZeroException处理除以零错误。
System.InvalidCastException处理类型转换错误。
System.OutOfMemoryException处理内存不足错误。
System.StackOverflowException处理栈溢出错误。

 24、文件的输入输出

(1)文件相关类

I/O 类描述
BinaryReader从二进制流读取原始数据。
BinaryWriter以二进制格式写入原始数据。
BufferedStream提供字节流的临时存储。
Directory用于操作目录结构。
DirectoryInfo用于对目录执行操作。
DriveInfo提供驱动器的信息。
File用于处理文件(例如:创建、删除、打开文件)。
FileInfo用于对文件执行操作(例如:获取文件信息、修改文件)。
FileStream用于文件中任何位置的读写。
MemoryStream用于随机访问存储在内存中的数据流。
Path对路径信息执行操作。
StreamReader用于从字节流中读取字符。
StreamWriter用于向一个流中写入字符。
StringReader用于读取字符串缓冲区。
StringWriter用于写入字符串缓冲区。

(2) FileStream类

参数描述
FileMode定义了打开文件的方式(例如:CreateOpen 等)。
FileAccess定义对文件的访问类型(例如:ReadWriteReadWrite)。
FileShare定义文件访问的共享模式(例如:NoneReadReadWrite)。

(3) FileMode类

枚举值描述
Append打开文件并将光标放在文件末尾,若文件不存在则创建。
Create创建一个新文件,若文件已存在,则覆盖文件。
CreateNew创建新文件,若文件已存在则抛出异常。
Open打开现有文件,若文件不存在则抛出异常。
OpenOrCreate打开现有文件,若文件不存在则创建新文件。
Truncate打开现有文件并截断为零字节大小。若文件不存在则抛出异常。

(4) FileAccess类

枚举值描述
Read仅允许读取文件。
Write仅允许写入文件。
ReadWrite允许同时读取和写入文件。

(5) FileShare类

枚举值描述
None不允许任何进程共享文件。
Read允许其他进程以只读方式访问文件。
Write允许其他进程以写方式访问文件。
ReadWrite允许其他进程以读写方式访问文件。
Delete允许其他进程删除文件。

25、特性

特性名称描述语法示例
规定特性用于为程序元素(如类、方法等)提供元数据。包括位置参数和命名参数。[attribute(positional_parameters, name_parameter = value, ...)]
AttributeUsage描述如何使用自定义特性,指定特性可以应用的目标。`[AttributeUsage(AttributeTargets.Class
Conditional用于标记条件方法,只有在指定的条件下才会执行。[Conditional("DEBUG")]
Obsolete标记已过时的方法或类,编译器会生成警告或错误提示。[Obsolete("Don't use OldMethod, use NewMethod instead", true)]
自定义特性声明自定义特性类必须派生自System.Attribute`[AttributeUsage(AttributeTargets.Class
自定义特性构建通过构造函数传递必需的参数,同时可以设置可选参数来扩展特性的信息。public class DeBugInfo : System.Attribute { public DeBugInfo(int bugNo, string developer, string lastReview) { ... } }
自定义特性应用使用自定义特性时,必须将其放置在目标元素的前面。[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")] class Rectangle { ... }

26、反射

内容描述
定义反射是程序访问、检测和修改自身状态或行为的能力。
组成程序集包含模块,模块包含类型,类型包含成员。反射提供了封装程序集、模块和类型的对象。
作用- 动态创建类型实例
- 绑定类型到现有对象
- 获取类型
- 调用类型方法,访问字段和属性
命名空间System.Reflection
优点- 提高程序灵活性和扩展性
- 降低耦合性,提高自适应能力
- 允许程序创建和控制任何类的对象,无需硬编码目标类
缺点- 性能问题:反射比直接代码慢
- 反射模糊程序内部逻辑,增加维护难度
- 反射代码较复杂,比直接代码难以理解
用途- 查看属性信息
- 审查集合中的类型,实例化这些类型
- 延迟绑定方法和属性
- 运行时创建新类型并执行任务
查看元数据使用System.Reflection.MemberInfo对象,初始化并发现类相关属性(attribute)。
示例自定义属性示例:
通过反射读取类MyClass的自定义属性HelpAttribute
示例代码展示了如何通过GetCustomAttributes()方法访问和输出附加的属性信息。
实例化对象通过反射读取和输出类Rectangle及其成员的方法和属性的元数据,输出调试信息。例如,DeBugInfo自定义属性可用于跟踪类成员及方法的调试信息。

27、属性

概念描述
属性 (Property)属性是类、结构和接口的命名成员,用来扩展域 (Field),通过访问器 (accessor) 让私有域的值可被读写或操作。
域 (Field)域是类或结构中的成员变量或方法。
访问器 (Accessors)访问器包含有助于获取(读取或计算)或设置(写入)属性的可执行语句,通常包括 getset 访问器,或仅一个访问器。
属性实例属性通过定义 getset 访问器来访问私有域。例如:public string Code { get { return code; } set { code = value; } }
抽象属性 (Abstract Properties)抽象类可以拥有抽象属性,这些属性需要在派生类中实现。例如,public abstract string Name { get; set; }
简化版抽象属性 (C# 6.0)使用简化的语法定义属性和自动实现的属性。例如:public string Code { get; set; } = "N.A";
属性用法示例在实例中,创建一个 Student 对象并通过属性访问和设置值,最终输出学生信息。例如:Student Info: Code = 001, Name = Zara, Age = 9
抽象类示例抽象类定义抽象属性,派生类实现这些属性,并使用访问器来设置和获取值。
C# 6.0 新特性使用简化的语法和自动属性初始化器,简化代码并提高可读性。

28、索引器

概念描述
索引器 (Indexer)索引器允许对象像数组一样被索引,通过 this 关键字定义,在类中允许使用数组访问运算符 [ ] 来访问对象实例的特定部分。
索引器语法一维索引器的语法为: element-type this[int index] { get { } set { } }。其中 getset 用于获取或设置指定索引的值。
索引器用途索引器类似于属性,但不需要名称,使用 this 关键字来访问。它通过 getset 访问器来定义,允许获取或设置对象实例中的特定值。
索引器示例this[int index] 用于访问 namelist 数组中的元素。例如:names[0] = "Zara" 设置值,names[0] 获取值。
索引器重载索引器可以被重载,允许使用不同类型的参数。例如,使用 int 类型和 string 类型的索引器,可以分别通过索引访问和按名称查找元素。
重载索引器示例在同一类中定义两个索引器:一个使用 int 类型来设置和获取 namelist 数组中的元素,另一个使用 string 类型来查找数组中指定名称的元素的索引。例如:public int this[string name] { get { } }
索引器应用索引器使得对象能够表现得像数组一样,通过数组的方式访问类中的成员数据。通过重载索引器,类可以处理不同类型的访问方式,提供灵活的操作接口。
示例结果执行代码时,输出索引器访问的内容。例如:Zara, Riz, Nuha, ...2 表示 "Nuha" 的索引位置。

29、委托

概念描述
委托 (Delegate)委托是一个引用类型变量,存储对某个方法的引用,允许动态地在运行时改变引用。它类似于 C 或 C++ 中的函数指针,常用于事件和回调方法的实现。
声明委托委托声明决定了可由该委托引用的方法。语法:delegate <return type> <delegate-name> <parameter list>,例如:public delegate int MyDelegate(string s);
实例化委托声明委托类型后,使用 new 关键字实例化委托,并将其与特定的方法关联。示例:printString ps1 = new printString(WriteToScreen);
委托实例化示例示例代码中,委托 NumberChanger 引用了两个方法 AddNumMultNum,并通过委托对象调用这些方法。结果展示了 num 的值发生了变化。
委托的多播委托对象可以使用 + 运算符合并,调用时会依次执行所有合并的委托。可以使用 - 运算符从合并的委托中移除某个委托。多播委托实现了一个方法调用列表。
多播示例示例中,使用 nc += nc2 合并两个委托,调用时依次执行 AddNumMultNum,输出 num 的变化。结果显示 num 从 10 变为 75。
委托用途委托可用于在运行时动态调用方法,常用于事件和回调机制。示例代码展示了如何使用 printString 委托分别将字符串打印到控制台和写入文件。
委托作为参数委托可以作为参数传递给其他方法,在方法中使用该委托调用指定的目标方法。示例代码中,sendString 方法接受委托 printString,并调用传递给它的具体方法(打印到控制台或写入文件)。
委托示例输出执行委托时,输出控制台信息:“The String is: Hello World”。

 30、事件

概念描述
事件 (Event)事件是程序对某些用户操作或系统生成的通知的响应。常见的事件包括按键、鼠标点击或移动。事件用于进程间通信,并要求应用程序在事件发生时做出响应。
事件与委托事件通过委托来处理。委托用于将事件处理程序与事件关联,通常使用发布-订阅模型(Publisher-Subscriber),其中发布器类发布事件,订阅器类响应事件。
发布器 (Publisher)发布器类包含事件和委托定义,并负责触发事件,通知订阅器类。
订阅器 (Subscriber)订阅器类响应事件,提供事件处理程序来处理事件。
声明事件在类中声明事件之前,首先声明事件的委托类型。事件使用 event 关键字声明,示例:public event BoilerLogHandler BoilerEventLog;
实例 1:事件的声明与使用示例中,EventTest 类定义了 NumManipulationHandler 委托和 ChangeNum 事件。在 SetValue 方法中触发事件,如果值发生变化则调用 OnNumChanged 方法触发事件。
实例 2:锅炉事件日志通过事件发布器类 DelegateBoilerEvent 和事件 BoilerEventLog,结合委托 BoilerLogHandler 处理锅炉的状态。事件处理程序会记录温度和压力信息,并将日志保存到文件和输出到控制台。
事件触发事件通过发布器调用触发方法 OnBoilerEventLog,检查事件是否已被订阅,如果订阅了事件,则调用所有订阅的处理方法(如日志记录器)。
事件订阅订阅器类通过 += 运算符订阅事件,例如 boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(Logger);,该方法被调用时会执行相应的处理。
事件输出示例执行事件时,输出以下信息:Logging Info: Temperature 100 Pressure 12 Message: O.K,并且日志写入文件 boiler.txt

31、集合

类名描述和用法
动态数组 (ArrayList)代表一个可以单独索引的有序集合,类似于数组,但大小动态调整,支持在任意位置添加、移除项目,提供动态内存分配、搜索和排序功能。
哈希表 (Hashtable)使用键来访问集合中的元素,键值对存储,每个元素都有一个键用于访问集合中的项目。
排序列表 (SortedList)结合了数组和哈希表的特性,支持通过键或索引访问项目。列表按键值排序,当使用索引访问时类似动态数组,当使用键访问时类似哈希表。
堆栈 (Stack)代表一个后进先出的对象集合,支持推入(添加元素)和弹出(移除元素)操作。
队列 (Queue)代表一个先进先出的对象集合,支持入队(添加元素)和出队(移除元素)操作。
点阵列 (BitArray)使用 1 和 0 表示的二进制数组,用于存储位数据,适用于位数未知的情况,支持通过整型索引访问每一位,索引从零开始。

32、泛型

概念/特性描述
泛型(Generic)允许延迟指定类或方法中的数据类型,直到实际使用时。它使得类或方法可以与任何数据类型一起工作,提高代码重用性和类型安全性。
泛型类实例通过数据类型的替代参数编写类的规范,在编译时根据类型生成代码。例如,MyGenericArray<T> 类使用泛型 T 来处理不同的数据类型。
泛型的特性1. 最大化代码重用。 2. 提高类型安全性。 3. 提升性能。 4. 创建泛型集合类。 5. 创建自定义泛型接口、类、方法、事件和委托。 6. 可以对泛型类进行约束以访问特定数据类型的方法。
泛型方法实例泛型方法允许使用类型参数声明方法,例如 Swap<T> 方法可以交换任意类型的两个变量的值。
泛型委托泛型委托允许使用类型参数定义委托。例如,NumberChanger<T> 委托可以用来定义处理不同类型数据的操作。实例中展示了使用泛型委托来实现对变量的操作。
示例代码输出- 泛型数组:输出整型数组 0 5 10 15 20 和字符数组 a b c d e
- 泛型方法:交换整数和字符的值,输出交换前后的结果。
- 泛型委托:输出 Value of Num: 35Value of Num: 175

33、匿名方法

(1)将委托加入对比

概念/特性描述
委托(Delegate)委托用于引用具有相同签名的方法,允许通过委托对象调用被引用的方法。
匿名方法(Anonymous Methods)匿名方法没有名称,只有方法体。它提供了一种传递代码块作为委托参数的技术,返回类型是通过方法主体的 return 语句推断出来的。
匿名方法语法通过 delegate 关键字创建委托实例并指定代码块。例如:NumberChanger nc = delegate(int x){ Console.WriteLine("Anonymous Method: {0}", x); };
委托调用方式委托可以通过匿名方法调用,也可以通过命名方法调用。例如,委托实例化后,可以使用 nc(10) 来调用匿名方法或命名方法。
示例代码1. 使用匿名方法创建委托实例并调用:nc(10) 输出 Anonymous Method: 10
2. 使用命名方法实例化委托并调用:nc(5) 输出 Named Method: 15nc(2) 输出 Named Method: 30

34、不安全代码

概念/特性描述
unsafe修饰符unsafe 修饰符标记的代码块允许使用指针变量,表示使用了指针的非托管代码。
指针变量指针是存储另一个变量地址的变量。声明格式:type *var-name;。例如,int* p; 表示指向整数的指针。
指针声明示例- int* p;:p 是指向整数的指针。
- double* p;:p 是指向双精度数的指针。
- int** p;:p 是指向整数指针的指针。
指针声明规则多个指针声明时,星号 * 只与基础类型一起声明,不能为每个指针单独加星号,例如:int* p1, p2, p3; 是正确的。
指针使用示例- int var = 20;
- int* p = &var;
示例输出:Data is: 20, Address is: 99215364
使用ToString检索数据值使用 ToString() 方法可以检索指针引用位置的数据。例如:p->ToString() 会输出指针所指向的数据。示例输出:Data is: 20, Data is: 20, Address is: 77128984
传递指针作为方法参数可以向方法传递指针作为参数。例如:交换两个整数的指针。示例输出:Before Swap: var1: 10, var2: 20, After Swap: var1: 20, var2: 10
使用指针访问数组元素使用指针访问数组时,需使用 fixed 关键字固定指针地址。例如:fixed(int *ptr = list),然后通过指针访问数组元素。输出显示数组地址和值。
编译不安全代码编译包含不安全代码时需使用 /unsafe 标志。例如:csc /unsafe prog1.cs在 Visual Studio 中,需要在项目属性中启用不安全代码。

35、多线程

类别描述
线程定义线程是程序的执行路径,具有独特的控制流。线程可以帮助应用程序执行复杂、耗时的操作。线程是轻量级进程,在现代操作系统中用于并行编程,节省 CPU 周期,提高应用程序效率。
线程生命周期1. 未启动状态:线程实例已创建,但未调用 Start 方法。
2. 就绪状态:线程准备运行,等待 CPU 周期。
3. 不可运行状态:例如调用 Sleep、Wait 或 I/O 操作阻塞。
4. 死亡状态:线程执行完毕或已中止。
主线程进程中第一个执行的线程,自动创建并执行。通过 Thread.CurrentThread 属性可访问主线程。
常用属性1. CurrentThread:获取当前线程。
2. IsAlive:指示线程是否处于活动状态。
3. IsBackground:指示线程是否为后台线程。
4. ThreadState:获取线程的状态。
常用方法1. Abort():终止线程。
2. Join():阻塞调用线程,直到某个线程结束。
3. Sleep():让线程暂停一段时间。
4. Start():启动线程执行。
5. Interrupt():中断线程。
创建线程通过继承 Thread 类并调用 Start() 方法创建线程。例如:
Thread childThread = new Thread(childref);
管理线程1. Sleep():暂停线程一段时间。
2. Interrupt():中断等待状态的线程。
销毁线程通过 Abort() 方法中止线程,抛出 ThreadAbortException 异常。此异常无法捕获,控制会转移到 finally 块。

36、字典

功能描述
定义Dictionary 是一个从一组键(Key)到一组值(Value)的映射结构,包含在System.Collections.Generic命名空间中。
键的要求键必须是唯一的,且不能为空引用(null)。如果值为引用类型,则可以为空值。
键和值类型Key 和 Value 可以是任何类型(如 string, int, 自定义类等)。
常用方法与属性
创建和初始化Dictionary<int, string> myDictionary = new Dictionary<int, string>();
添加元素myDictionary.Add(1, "C#");
通过Key查找元素if(myDictionary.ContainsKey(1)) { Console.WriteLine("Key:{0}, Value:{1}", "1", myDictionary[1]); }
通过KeyValuePair遍历元素foreach(KeyValuePair<int, string> kvp in myDictionary) { Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value); }
仅遍历键Dictionary<int, string>.KeyCollection keyCol = myDictionary.Keys; foreach (int key in keyCol) { Console.WriteLine("Key = {0}", key); }
仅遍历值Dictionary<int, string>.ValueCollection valueCol = myDictionary.Values; foreach (string value in valueCol) { Console.WriteLine("Value = {0}", value); }
移除指定键值myDictionary.Remove(1); if(!myDictionary.ContainsKey(1)) { Console.WriteLine("不存在 Key : 1"); }
常见属性和方法
Comparer获取用于确定字典中键是否相等的 IEqualityComparer
Count获取字典中键/值对的数量。
Item获取或设置与指定键相关联的值。
Keys获取字典中所有键的集合。
Values获取字典中所有值的集合。
Add将指定的键和值添加到字典中。
Clear从字典中移除所有键和值。
ContainsKey检查字典是否包含指定的键。
ContainsValue检查字典是否包含指定的值。
GetEnumerator返回用于遍历字典的枚举器。
GetType获取字典实例的类型。
Remove从字典中移除指定键的值。
ToString返回字典的字符串表示。
TryGetValue获取与指定键关联的值。

四、实操

1、Hello World

/* 
 * 该文件是一个简单的C#控制台应用程序,主要用于演示如何在控制台输出“Hello World”。
 * 程序使用了System命名空间下的基本功能来进行输出操作,并等待用户输入以结束程序。
 * 
 * 文件包含以下部分:
 * - 引用必要的命名空间
 * - 程序入口Main方法
 * - 控制台输出功能
 */

// 导入C#中常用的系统库,用于提供基本的功能
using System; // 提供基本的类和基类支持,例如Console类
using System.Collections.Generic; // 提供泛型集合的接口和类
using System.Linq; // 提供对集合进行查询操作的功能(LINQ)
using System.Text; // 提供对文本处理和编码转换的功能
using System.Threading.Tasks; // 提供对异步编程和任务并行的支持

// 定义命名空间,用于组织代码和避免名称冲突
namespace ConsoleApp1 // 定义项目的命名空间,命名为ConsoleApp1
{
    // 定义一个类,类是C#中代码的基本结构单元
    class Program // 定义Program类,通常是程序的入口点
    {
        // 定义Main方法,是程序执行的入口
        static void Main(string[] args) // Main方法,程序从这里开始运行
        {
            // 使用Console类输出一行文本到控制台
            Console.WriteLine("Hello World"); // 输出"Hello World"到控制台

            // 等待用户按下回车键,以便查看控制台输出
            Console.ReadLine(); // 阻止程序立即关闭,等待用户输入
        }
    }
}

(1)入口函数

Main()是默认的入口函数,可以被修改,但我不告诉你怎么修改。

我想说的是他的特殊性,通常来说,一个静态的函数,都是需要现行调用的,不能直接运行。但入口函数不一样,运行程序会被直接调用,不需要显性调用。

入口函数有且只有一个。

2、模块化

(1)创建文件夹与cs文件

双击 .sln 文件(解决方案文件)来打开解决方案或项目 》 右键解决方案名称添加文件夹 》 右键文件夹添加类

(2)修改cs文件名,类名可自定义

(3)命名空间

命名空间也可以自定义,如可以跟其他文件的命名空间名称一样,或者写其他字符。

规范1:所有文件中的命名空间都跟项目名称一致,这样在导入不同模块时,是需要使用同个命名空间即可,适合小型项目。

规范2:命名空间.目录路径,  这种方式适合大型项目,在引用时,需要 using 命名空间.目录路径,这样就可以导入该空间下的类或方法了。

 (4)引用定义好的类

在Hello.cs文件中,写入:

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

namespace ConsoleApp1
{
    class HelloWorld
    {
        public void PrintMessage()
        {
            Console.WriteLine("Hello World");
        }
    }
}

在Program.cs中写入:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApp1; // 使用命名空间

namespace ConsoleApp1 
{

    class Program
    {
        static void Main(string[] args) 
        {
            // 创建 HelloWorld 类的实例
            HelloWorld helloWorld = new HelloWorld();

            // 调用 PrintMessage 方法
            helloWorld.PrintMessage();

            Console.ReadLine(); 
        }
    }
}

可正常运行:

 

3、实现MC服务器

(1)下载第三方应用程序

Download - 胡工科技官网 (hsltechnology.cn)

 (2)加载应用程序的dll库

解压安装包》找到以下库

 使用VS添加库,详见本文:开发环境》加载库

(3)查看接口文档

MelsecMcServer 类 (hslcommunication.cn)

(4)编程

实现功能:启动MC服务器,往D0寄存器写入“AA”,100ms后往D0寄存器写入“00”,接着读取D10数据,5秒后重复该动作。

新增McServer.cs,写入以下代码:

using System;
using System.Text;
using System.Timers;
using HslCommunication.Profinet.Melsec;
using TimerThreading = System.Threading.Timeout;

namespace ConsoleApp1
{
    class McServer
    {
        private readonly string writeAddress;
        private readonly string writeValue;
        private readonly string readAddress;
        private MelsecMcServer mcServer;
        private Timer writeTimer;
        private int elapsedTime;  // 用来记录已延迟的时间
        private int delayTime;  // 延迟时间
        private int existTime;  // 指令存在时间

        public McServer(string writeAddress, string writeValue, string readAddress)
        {
            this.writeAddress = writeAddress;
            this.writeValue = writeValue;
            this.readAddress = readAddress;
            this.elapsedTime = 0; // 初始化已延迟时间
        }

        // 启动服务器时,增加了 exist_time 和 delayTime 参数
        public void StartServer(int port, int existTime, int delayTime)
        {
            try
            {
                // 初始化并启动 MC 服务器
                mcServer = new MelsecMcServer
                {
                    ActiveTimeSpan = TimeSpan.FromHours(24) // 运行1小时
                    // ActiveTimeSpan = TimerThreading.InfiniteTimeSpan  //永久运行,可能不支持
                };
                mcServer.ServerStart(port);
                Console.WriteLine($"MC 3E Frame Server started on port {port}.");

                // 保存延迟时间
                this.delayTime = delayTime;
                this.existTime = existTime;

                // 初始化定时器,使用 exist_time 控制定时器触发的间隔
                writeTimer = new Timer(delayTime);
                writeTimer.Elapsed += WriteTimer_Elapsed; // 每次定时器触发时执行写入操作
                writeTimer.AutoReset = true;
                writeTimer.Start();

                Console.WriteLine("Write timer started.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to start server: {ex.Message}");
            }
        }

        // 停止服务器
        public void StopServer()
        {
            try
            {
                writeTimer.Stop();
                mcServer.ServerClose();
                Console.WriteLine("Server and timer stopped.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error stopping server: {ex.Message}");
            }
        }

        // 写入并延迟修改寄存器的操作
        private void WriteTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                // 每次定时器触发,增加已延迟时间
                elapsedTime += (int)writeTimer.Interval;

                // 写入AA到寄存器
                mcServer.Write(writeAddress, Encoding.ASCII.GetBytes(writeValue));
                Console.WriteLine($"Wrote '{writeValue}' to {writeAddress}");

                // 延时
                System.Threading.Thread.Sleep(existTime);


                // 延迟时间(delayTime)后修改寄存器为00
                mcServer.Write(writeAddress, Encoding.ASCII.GetBytes("00"));
                Console.WriteLine($"Changed {writeAddress} to '00'");

                // 读取寄存器值并打印调试信息
                var readResult = mcServer.Read(readAddress, (ushort)writeValue.Length);
                if (readResult.IsSuccess)
                {
                    string readValue = Encoding.ASCII.GetString(readResult.Content);
                    Console.WriteLine($"Successfully read '{readValue}' from {readAddress}");
                }
                else
                {
                    Console.WriteLine($"Failed to read from {readAddress}: {readResult.Message}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error in WriteTimer_Elapsed: {ex.Message}");
            }
        }
    }
}

在Program.cs文件中写入

using System;
using HslCommunication.Profinet.Melsec;
using System.Timers;

namespace ConsoleApp1
{
    class Program
    {
        static McServer mcServer; // MC 服务器实例

        static void Main(string[] args)
        {
            // 打印欢迎信息
            HelloWorld helloWorld = new HelloWorld();
            helloWorld.PrintMessage();

            // 初始化并启动 3E 帧 MC 服务器
            mcServer = new McServer("D0", "AA", "D10");
            mcServer.StartServer(9600, 100, 5000);

            Console.WriteLine("Press Enter to exit...");

            // 等待用户按下 Enter 键退出
            Console.ReadLine();

            // 停止服务器和定时器
            mcServer.StopServer();
        }
    }
}

(5)运行

(6)命名空间那些事儿

对于内置模块,可以不使用using关键字引入,可直接通过完整路径使用,如上述代码System.Threading.Thread.Sleep(1000);

对于同个命名空间,可以不使用using关键字引入,可直接在该命名空间下使用属于该命名空间的类和方法,无论他在哪里,如上述代码 static McServer mcServer;

对于命名空间别名,用法如using TimerThreading = System.Threading.Timeout; 即TimerThreading为System.Threading.Timeout的别名,用于解决不同模块的Timeout的同名冲突。

(7)构造函数

class McServer的构造函数是public McServer,即如果你想给这个class McServer类传参,需要构造函数public McServer,它的名字必须和类名相同,且没有返回类型(连 void 也没有)。

(8)static关键字

static定义了一个静态变量,因为Main的方法是static,只能访问同样是 static 的类成员。原因是 static 方法属于类,而非某个对象实例,因此它不能访问非静态成员(需要实例来调用)。我们需要将 mcServer 声明为 static,以便与 Main 方法在同一静态上下文中使用。如果为非静态实例,将会出现以下报错:

4、继承

问大家一个问题,爸爸的爸爸叫什么?当你很快速的回答这个问题后,你就发现,你有了继承的意识。

新增McServer_inherit.cs文件写入:



namespace ConsoleApp1
{
    class McServer_Base : McServer  // 基本继承
    {
        public McServer_Base(string writeAddress, string writeValue, string readAddress)
            : base(writeAddress, writeValue, readAddress)
        {
        }
    }

    class McServer_Override : McServer  // 方法重写
    {
        public McServer_Override(string writeAddress, string writeValue, string readAddress)
            : base(writeAddress, writeValue, readAddress)
        {
        }

        public override void StartServer(int port, int existTime, int delayTime)
        {
            System.Console.WriteLine("Override StartServer");
            base.StartServer(port, existTime, delayTime);
        }
    }

    class McServer_New : McServer  // 方法隐藏
    {
        public McServer_New(string writeAddress, string writeValue, string readAddress)
            : base(writeAddress, writeValue, readAddress)
        {
        }

        public new void StartServer(int port, int existTime, int delayTime)
        {
            System.Console.WriteLine("New StartServer");
            base.StartServer(port, existTime, delayTime);
        }
    }

    class McServer_Constructor : McServer  // 构造函数继承
    {
        public McServer_Constructor(string writeAddress, string writeValue, string readAddress)
            : base(writeAddress, writeValue, readAddress)
        {
            System.Console.WriteLine("McServer_Constructor initialized");
        }
    }

    abstract class AbstractServer
    {
        public abstract void StartServer(int port, int existTime, int delayTime);
        public abstract void StopServer();
    }

    class McServer_Abstract : AbstractServer  // 抽象类继承
    {
        private McServer mcServer;

        public McServer_Abstract(string writeAddress, string writeValue, string readAddress)
        {
            mcServer = new McServer(writeAddress, writeValue, readAddress);
        }

        public override void StartServer(int port, int existTime, int delayTime)
        {
            mcServer.StartServer(port, existTime, delayTime);
        }

        public override void StopServer()
        {
            mcServer.StopServer();
        }
    }

    interface IServer
    {
        void StartServer(int port, int existTime, int delayTime);
        void StopServer();
    }

    class McServer_Interface : IServer  // 接口继承
    {
        private McServer mcServer;

        public McServer_Interface(string writeAddress, string writeValue, string readAddress)
        {
            mcServer = new McServer(writeAddress, writeValue, readAddress);
        }

        public void StartServer(int port, int existTime, int delayTime)
        {
            mcServer.StartServer(port, existTime, delayTime);
        }

        public void StopServer()
        {
            mcServer.StopServer();
        }
    }

    class McServer_Parent: McServer
    {
        public McServer_Parent(string writeAddress, string writeValue, string readAddress)
: base(writeAddress, writeValue, readAddress) // 调用父类的构造函数
        {
            System.Console.WriteLine("McServer_Parent constructor called.");
        }
        public override void StartServer(int port, int existTime, int delayTime)
        {
            System.Console.WriteLine("Starting server in McServer_Parent");
            base.StartServer(port, existTime, delayTime);
        }
    }

    class McServer_MultiLevel : McServer_Parent  // 多层继承
    {
        public McServer_MultiLevel(string writeAddress, string writeValue, string readAddress)
    : base(writeAddress, writeValue, readAddress) // 调用父类的构造函数
        {
            System.Console.WriteLine("McServer_MultiLevel constructor called.");
        }
    }

    sealed class McServer_Sealed  // 防止继承
    {
        private readonly McServer mcServer;

        public McServer_Sealed(string writeAddress, string writeValue, string readAddress)
        {
            mcServer = new McServer(writeAddress, writeValue, readAddress);
        }

        public void StartServer(int port, int existTime, int delayTime)
        {
            mcServer.StartServer(port, existTime, delayTime);
        }

        public void StopServer()
        {
            mcServer.StopServer();
        }
    }

}

在Program.cs文件中依次调用

using System;
using HslCommunication.Profinet.Melsec;
using System.Timers;

namespace ConsoleApp1
{
    class Program
    {
        // static McServer mcServer; // MC 服务器实例
        // static McServer_Base mcServer;  // 0基本继承
        // static McServer_Override mcServer;  // 1方法重写
        // static McServer_New mcServer;  // 2方法隐藏
        // static McServer_Constructor mcServer;  // 3构造函数继承
        // static McServer_Abstract mcServer;  // 4抽象类继承
        // static McServer_Interface mcServer;  // 5接口继承
        // static McServer_MultiLevel mcServer;  // 6多层继承
        static McServer_Sealed mcServer;  // 7防止继承

        static void Main(string[] args)
        {
            // 打印欢迎信息
            HelloWorld helloWorld = new HelloWorld();
            helloWorld.PrintMessage();

            // 初始化并启动 3E 帧 MC 服务器
            //mcServer = new McServer("D0", "AA", "D10");
            //mcServer = new McServer_Base("D0", "AA", "D10");  // 0基本继承
            //mcServer = new McServer_Override("D0", "AA", "D10");  // 1方法重写
            //mcServer = new McServer_New("D0", "AA", "D10");  // 2方法隐藏
            //mcServer = new McServer_Constructor("D0", "AA", "D10");  // 3构造函数继承
            //mcServer = new McServer_Abstract("D0", "AA", "D10");  // 4抽象类继承
            //mcServer = new McServer_Interface("D0", "AA", "D10");  // 5接口继承
            //mcServer = new McServer_MultiLevel("D0", "AA", "D10");  // 6多层继承
            mcServer = new McServer_Sealed("D0", "AA", "D10");  // 7防止继承

            mcServer.StartServer(9600, 100, 5000);

            Console.WriteLine("Press Enter to exit...");

            // 等待用户按下 Enter 键退出
            Console.ReadLine();

            // 停止服务器和定时器
            mcServer.StopServer();
        }
    }
}

需要改造一下父类方法才能重写方法,在McServer.cs文件中:

public void StartServer(int port, int existTime, int delayTime)

改为

public virtual void StartServer(int port, int existTime, int delayTime)

(1)继承的注意事项

当父类有构造函数时,需要继承构造函数,如:

        public McServer_Base(string writeAddress, string writeValue, string readAddress)
            : base(writeAddress, writeValue, readAddress)
        {
        }

当父类方法被重写时,如果还想继续使用父类方法,需要显性调用,如:

base.StartServer(port, existTime, delayTime);

当父类方法被重写,而且多个子类都在继续使用父类方法,那么会被调用多次,但父类方法会被实例化一次,如多层继承的:

base.StartServer(port, existTime, delayTime);

 方法隐藏跟重写方法有些相似。方法隐藏是指子类定义了与父类同名的方法,但没有使用 override 关键字,而是使用 new 关键字显式地隐藏父类中的方法。在这种情况下,子类的方法会隐藏父类的方法,而不是重写父类的方法。起到一个效果:即使父类的方法不是 virtual,子类依然可以使用 new 关键字来隐藏父类的方法。这种情况下,隐藏的是父类的方法,而不是重写父类的方法。

构造函数继承即在继承父类构造函数后,可添加一些行为。

对于抽象类继承,可以强制子类实现抽象方法: 抽象类通过定义抽象方法,强制继承该类的子类必须提供这些方法的实现。这样可以确保所有子类都遵循相同的接口规范。可以统一接口: 通过抽象类,父类提供了一种统一的接口,子类可以根据自己的需求实现这些接口。这样可以让代码结构更加清晰,并且容易扩展。可以提供共用的代码: 抽象类除了抽象方法,还可以包含普通的已实现方法。在这种情况下,子类继承抽象类不仅可以实现抽象方法,还可以直接使用父类中已经实现的方法,从而减少代码重复。这得用代码来解释:

// 抽象类,定义了必须由子类实现的抽象方法
abstract class AbstractServer
{
    // 抽象方法,子类必须重写此方法并提供具体实现
    public abstract void StartServer(int port, int existTime, int delayTime);
    public abstract void StopServer();
}

// 具体的子类,继承自抽象类并实现其抽象方法
class McServer_Abstract : AbstractServer  // 抽象类继承
{
    private McServer mcServer;

    // 构造函数:子类在构造时实例化 McServer 对象
    public McServer_Abstract(string writeAddress, string writeValue, string readAddress)
    {
        mcServer = new McServer(writeAddress, writeValue, readAddress);
    }

    // 重写抽象方法 StartServer,提供具体实现
    public override void StartServer(int port, int existTime, int delayTime)
    {
        mcServer.StartServer(port, existTime, delayTime); // 调用 McServer 类的方法
    }

    // 重写抽象方法 StopServer,提供具体实现
    public override void StopServer()
    {
        mcServer.StopServer(); // 调用 McServer 类的方法
    }
}

接口继承和抽象类继承实在太像了,直接上对比:

特性接口(Interface)抽象类(Abstract Class)
继承方式可以被多个类实现(支持多重继承)只能单继承一个抽象类
方法实现只能声明方法,不能提供实现,除非是 C# 8.0 后的默认实现可以声明抽象方法,也可以提供方法实现
成员限制只能包含方法签名、属性、事件、索引器等声明可以包含方法、字段、属性、事件、构造函数等
访问修饰符所有成员默认为 public,不能指定其他访问修饰符可以使用任何访问修饰符(private、protected、public)
构造函数不能有构造函数可以有构造函数
字段不能包含字段可以包含字段
适用场景用于定义行为规范,支持不同类的统一接口,适合松耦合设计用于共享部分实现和公共基础设施,适合有共性的类

C#没有直接实现多重继承的方式,但可由接口继承来实现。PS:多重继承 是指一个类可以同时继承多个父类的特性和行为。这意味着子类可以从多个父类继承方法、属性和其他成员,允许类从多个来源组合其功能。这种继承方式是面向对象编程中的一种常见特性,但并非所有编程语言都支持多重继承。

防止继承表明这个类,不会被其他类继承。

5、实现TCPclient

新增TCP.cs

using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace ConsoleApp1
{
    public class TcpClientWrapper : IDisposable
    {
        private TcpClient client;
        private NetworkStream stream;
        private bool disposed = false; // 用于标记是否已释放资源
        private readonly ManualResetEvent connectDone = new ManualResetEvent(false); // 用于实现连接超时
        private readonly int connectTimeout = 5000; // 连接超时时间(毫秒)

        // 初始化TCP客户端并连接到服务器。
        public TcpClientWrapper(string serverIP, int serverPort)
        {
            try
            {
                client = new TcpClient
                {
                    ReceiveTimeout = 30000, // 接收超时时间(30秒)
                    SendTimeout = 30000,   // 发送超时时间(30秒)
                    NoDelay = true         // 禁用Nagle算法,减少延迟
                };

                Console.WriteLine("正在连接到服务器...");
                var asyncResult = client.BeginConnect(serverIP, serverPort, null, null);
                if (!asyncResult.AsyncWaitHandle.WaitOne(connectTimeout))
                {
                    throw new TimeoutException("连接服务器超时!");
                }

                client.EndConnect(asyncResult); // 完成连接
                stream = client.GetStream();
                Console.WriteLine("已连接到服务器,长连接已建立!");
            }
            catch (SocketException ex)
            {
                Console.WriteLine($"连接服务器失败: {ex.Message}");
                Dispose();
                throw;
            }
            catch (TimeoutException ex)
            {
                Console.WriteLine($"连接超时: {ex.Message}");
                Dispose();
                throw;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"未知错误: {ex.Message}");
                Dispose();
                throw;
            }
        }

        // 发送消息到服务器。
        public void Send(string message)
        {
            if (disposed)
            {
                throw new ObjectDisposedException(nameof(TcpClientWrapper));
            }

            if (stream == null || !client.Connected)
            {
                throw new InvalidOperationException("尚未连接到服务器。");
            }

            try
            {
                byte[] data = Encoding.UTF8.GetBytes(message);
                stream.Write(data, 0, data.Length);
                Console.WriteLine($"发送消息: {message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"发送消息失败: {ex.Message}");
            }
        }

        // 从服务器接收消息。
        public string Receive()
        {
            if (disposed)
            {
                throw new ObjectDisposedException(nameof(TcpClientWrapper));
            }

            if (stream == null || !client.Connected)
            {
                throw new InvalidOperationException("尚未连接到服务器。");
            }

            try
            {
                byte[] buffer = new byte[1024];
                int bytesRead = stream.Read(buffer, 0, buffer.Length);
                string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                Console.WriteLine($"接收到消息: {response}");
                return response;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"接收消息失败: {ex.Message}");
                return string.Empty;
            }
        }

        // 关闭连接并释放资源。
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this); // 告诉GC不需要再调用终结器
        }

        // 释放资源的核心方法
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    // 释放托管资源
                    if (stream != null)
                    {
                        stream.Close();
                        stream = null;
                    }

                    if (client != null)
                    {
                        client.Close();
                        client = null;
                    }
                }

                // 如果有非托管资源,可以在这里释放
                disposed = true;
                Console.WriteLine("已关闭连接并释放资源。");
            }
        }

        // 析构函数,用于释放资源
        ~TcpClientWrapper()
        {
            Dispose(false);
        }
    }
}

更新Program文件:

using System;
using HslCommunication.Profinet.Melsec;
using System.Timers;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string serverIP = "192.168.66.188";
            int serverPort = 5000;

            try
            {
                using (TcpClientWrapper tcpClient = new TcpClientWrapper(serverIP, serverPort))
                {
                    while (true)
                    {
                        Console.Write("请输入发送消息 (输入 'exit' 退出): ");
                        string message = Console.ReadLine();

                        if (message?.ToLower() == "exit")
                        {
                            break;
                        }

                        tcpClient.Send(message);
                        string response = tcpClient.Receive();
                        Console.WriteLine($"服务器响应: {response}");
                    }
                }
            }
            catch (TimeoutException ex)
            {
                Console.WriteLine($"连接超时: {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"发生异常: {ex.Message}");
            }
        }
    }
}

(1)using方法

using方法除了引入命名空间,还是可以用于自动释放资源。如:

using (TcpClientWrapper tcpClient = new TcpClientWrapper(serverIP, serverPort))

 此时,using 语句的主要目的是:资源管理,即自动调用实现了 IDisposable 接口的类的 Dispose 方法。

这里需要注意:

Dispose 方法的名称是固定的,可以在这个方法里面加入其他资源释放方法。

所引用的类,需要继承IDisposable,即必须实现 IDisposable 接口。

(2)析构函数

析构函数,是 C# 中的一种特殊方法,用于对象销毁时执行清理操作。如:

 ~TcpClientWrapper()

析构函数在垃圾回收器 (Garbage Collector, GC) 释放对象时被调用,用于释放非托管资源或执行一些清理工作。

(3)内存释放机制对比

特点析构函数IDisposable 接口
调用方式自动调用:由垃圾回收器在对象被销毁时自动触发。手动调用:需要显式调用 Dispose 方法,或者使用 using 语句。
执行时间确定性不确定性:无法保证析构函数的执行时间,因为垃圾回收的时机不可控。确定性:Dispose 方法立即执行,适合需要确定释放资源的场景。
性能成本可能增加垃圾回收器的负担,频繁调用可能对性能产生负面影响。性能友好,资源可以及时释放,不会额外增加垃圾回收的负担。
推荐性适合作为资源清理的最后保障机制,不建议单独依赖。微软推荐使用 IDisposable 接口来清理资源,符合 .NET 开发最佳实践。

(4)同名方法其实是不同方法

以下是名称相同的方法,但是因签名不一样而不同的方法:

        // 关闭连接并释放资源。
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this); // 告诉GC不需要再调用终结器
        }

        // 释放资源的核心方法
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    // 释放托管资源
                    if (stream != null)
                    {
                        stream.Close();
                        stream = null;
                    }

                    if (client != null)
                    {
                        client.Close();
                        client = null;
                    }
                }

                // 如果有非托管资源,可以在这里释放
                disposed = true;
                Console.WriteLine("已关闭连接并释放资源。");
            }
        }

在 C# 中,方法的合法性由方法签名(方法名+参数列表)决定。public void Dispose() 方法没有参数。protected virtual void Dispose(bool disposing) 方法有一个 bool 类型的参数。这两者是不同的方法,因此在同一个类中可以同时存在。

(5)托管资源与非托管资源

特性托管资源非托管资源
管理方式由垃圾回收器自动管理需要显式管理,通常通过 Dispose 或析构函数处理
资源类型常见例子:字符串 (string)、数组 (int[])、集合 (List<>Dictionary<>)常见例子:文件 (FileStream)、数据库连接 (SqlConnection)、图形对象 (Bitmap)、套接字 (Socket)
释放方式无需显式释放显式调用释放方法(Dispose),如 stream.Dispose()
风险无需担心资源泄漏未释放可能导致内存泄漏或句柄耗尽,影响系统稳定性

(6)释放系统资源的操作方式

                    // 释放托管资源
                    if (stream != null)
                    {
                        stream.Close();
                        stream = null;
                    }

                    if (client != null)
                    {
                        client.Close();
                        client = null;
                    }

stream.Close() 用于关闭 NetworkStream 对象,释放与其关联的系统资源,如缓冲区或文件描述符。client.Close() 用于关闭 TcpClient 对象,断开底层 TCP 连接,释放与之关联的系统资源(如套接字句柄)。置为 null,是为了告诉垃圾回收器(GC),该对象已经不再需要,可以回收其内存,方便垃圾回收器清理无用的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion King

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值