C#基础
什么是.NET
.NET 就是微软出的一个“开发平台”——一套工具、库、运行环境,帮助程序员快速开发各种类型的应用。
下载地址:https://dotnet.microsoft.com/zh-cn/download
入门教程:https://dotnet.microsoft.com/zh-cn/learn/dotnet/hello-world-tutorial/create
特性
- 语言集成查询(LINQ)
- 基于任务的异步编程模型,支持使用await、async、await foreach。
- 模式匹配
- 字符串插值和原始字符串字面量
- 可以为null的类型或不可为null的类型
- 拓展方法
- 本地函数
- 属性和索引器
- 记录
- 元组
- 属性
面向对象语言的特性
- 多态
- 封装
- 继承
- 抽象
C#程序结构
C#程序由一个或多个文件组成,每个文件包含零个或多个命名空间,C#源代码文件后缀为.cs。
命名空间包含类、结构、接口、枚举、委托或其他命名空间。
//使用其他命名空间或类
using System;
using static System.DateTime;
//定义一个命名空间按
namespace HelloWorldTest
{
class HelloWorldTest
{
public static void Main(string[] args)
{
Console.WriteLine("The current time is "+ DateTime.Now);
Console.WriteLine("hello world");
int a = 1;
int b = 2;
Console.WriteLine(a + b);
}
}
}语法基础
- C#是大小写敏感的
- 所有的语句和表达式必须以分号
;结尾。 - 程序的执行从Main方法开始。
- 与Java不同的是,文件名可以不同于类的名称。
上下文关键字
上下文关键字是指**只有在特定语法结构中才有“超能力”**的关键字,在一般情况下可以作为标识符使用。
常见的上下文关键字如下:
| 关键字 | 出现场景 | 说明 |
|---|---|---|
| from | LINQ 查询 | from item in list |
| where | LINQ 查询 | where 条件 |
| select | LINQ 查询 | select item |
| async | 方法声明 | async Task |
| await | 异步代码 | await xxx |
| record | 定义record类型 | record Person |
| partial | 类/方法拆分 | partial class |
| init | 属性只在初始化 | init-only setter |
打印控制台
Console.WriteLine("hello world");
//模板字符串
Console.WriteLine($"Hello {name.ToUpper()}!");顶级语句(Top-level Statements)
顶级语句是C#9.0后推出的语法糖,允许不用写class Program,不用写static void Main(),直接写代码,自动就是入口。适用于小型化项目、脚本化项目。
- 传统写法
using System;
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}- 顶级语句(C# 9.0+)
using System;
Console.WriteLine("Hello World!");顶级语句的底层其实是自动生成Main方法,编译器会补全上必要的内容。
using System;
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}顶级语句的限制
- 文件限制:顶级语句的源文件一个项目中只能有一个。在多个源文件中存在顶级语句的化编译器会报错。
- 程序入口:顶级语句会自动生成Main方法并作为程序入口,因此不能再重复定义Main方法入口。
- 作用域:在顶级语句中定义的变量、方法可以在整个文件中访问。
结构体Struct
在C#中,结构体Struct是一种值类型,用于组织数据结构,使得在一个变量中存储各种数据类型,可以是值类型或引用类型。
需要注意的是,结构体Struct是值类型。这意味着:
- 在赋值或传参时,会整体拷贝一份
- 但struct内的引用类型,拷贝的是指针,不是对象本身。
//定义结构体
struct Team
{
int a;
float b;
bool c;
public string[] Members;
}
Team t=new Team();//所有字段被赋予类型初始默认值结构体的几种创建方式
c#规定不能自定义结构体的无参构造函数(会自动创建一个),因此结构体有自己的创建方式。
| 方式 | 特点 | 备注 |
|---|---|---|
| new Struct() | 字段全是默认值 | 不会调用你写的构造函数 |
| new Struct(参数) | 调用你写的构造函数 | 初始化自定义 |
| Struct s; | 不初始化 | 所有字段必须手动赋值后才能用 |
类Class
在 C# 中,**类(class)**是一个定义对象(object)模板的结构,用来封装数据(字段)和行为(方法)。它是面向对象编程(OOP)的核心构建块之一。
在C#中,类成员变量和类本身的默认访问修饰符是不同的:
- 类成员变量的默认访问修饰符:
private
- 类的默认访问修饰符:
internal(仅限于顶级类)private(对于嵌套类)
// 顶级类
class MyClass // 等价于 internal class MyClass
{
int number; // 等价于private int number
}
// 嵌套类
class OuterClass
{
class InnerClass // 等价于 private class InnerClass
{
}
}虽然C#中一个文件可以定义多个命名空间或类,但为了提高代码质量和团队协作效率,一般只会在一个文件一个类,避免一个文件多个命名空间。
类属性与访问控制器
在 C# 中,“类属性(Property)”是对字段(Field)的一种封装形式,它允许你控制字段的访问,并在设置或获取值时执行逻辑。而“访问器(Accessor)”是属性中的 get 和 set 关键字,用于定义读取和写入的行为。
基本语法
public class Person
{
private string name; // 私有字段
public string Name // 公有属性
{
get { return name; } // 访问器
set { name = value; } // 访问器
}
//带逻辑的访问器
private int age;
public int Age
{
get { return age; }
set
{
if (value >= 0)
age = value;
else
throw new ArgumentException("年龄不能为负数");
}
}
//访问控制修饰符
public string Name { get; private set; } // 外部可读,内部可写
}自动实现
public string Name { get; set; }只读/只写属性
public int Age { get; } // 只读属性
public string Password { set; } // 只写属性带逻辑的访问器
private int age;
public int Age
{
get { return age; }
set
{
if (value >= 0)
age = value;
else
throw new ArgumentException("年龄不能为负数");
}
}初始化只读属性
public string ID { get; } = Guid.NewGuid().ToString(); // 构造时赋值,只读索引器
C#的索引器允许你像使用数组一样通过下标/索引访问对象的属性。它的作用是让类或结构像集合一样使用方括号 []来读取或写入内部数据,而不必显式调用方法。
基本语法
class Sample
{
private int[] data = new int[10];
// 索引器定义
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
//使用索引器访问对象内部数据
Sample s = new Sample();
s[0] = 42;
Console.WriteLine(s[0]); // 输出:42Class和Struct的区别
| 特性 | class | struct |
|---|---|---|
| 存储 | 引用类型(heap) | 值类型(stack) |
| 赋值 | 传引用 | 传值(复制) |
| 继承 | 支持继承 | 不支持继承(只能接口) |
| 默认构造 | 可以有无参构造 | 必须手动赋值所有字段 |
| 内存分配 | GC管理 | 栈上分配或嵌套存储 |
| 存储开销 | 多一点(header, GC info) | 小而紧凑 |
- struct 用来表示“小而简单”的值。使用原则是:数据小、不可变、无副作用、性能极致。
- class 用来表示“复杂且有行为”的对象。使用原则是:数据复杂、功能多、需要共享、不可避免GC。
继承
类的继承
一个类可以继承另一个类,被称为基类(父类)和派生类(子类)。
C#不支持多重继承,即一个类只能有一个基类,一个类可以实现多个接口。
即一个类可以实现多个接口,只能继承自一个类。
using System;
namespace ClassTest
{
class BaseClass
{
public void baseMethod()
{
Console.WriteLine("BaseClass");
}
}
class DerivedClass : BaseClass
{
public void derivedMethod()
{
base.baseMethod();
Console.WriteLine("DerivedClass");
}
}
class ClassTest
{
public void run()
{
BaseClass b = new BaseClass();
DerivedClass d = new DerivedClass();
b.baseMethod();
d.derivedMethod();
}
}
}在C#中使用base.调用父类方法,使用base()调用父类构造方法。
接口的继承
接口可以继承一个或多个接口,类可以实现一个或多个接口。
namespace InterfaceTest
{
interface InterfaceA
{
void methodA();
}
interface InterfaceB
{
void methodB();
}
class ImplentClass : InterfaceA, InterfaceB
{
public void methodA()
{
Console.WriteLine("methodA");
}
public void methodB()
{
Console.WriteLine("methodB");
}
public void test()
{
methodA();
methodB();
}
}
public class InterfaceTest
{
public void run()
{
new ImplentClass().test();
}
}
}多态
在 C# 中,“多态”(Polymorphism)是面向对象编程(OOP)的一个核心特性,意思是**“多种形态”。它允许对象以多种不同的方式表现自己,主要体现在方法重写(Override)和接口实现**两个方面。
多态分为静态多态性、动态多态性。
静态多态性
静态多态性是指在编译期即确定调用的对象与方法,通过方法重载、运算符重载实现。方法名相同,但参数不同(数量、类型或顺序)。
动态多态性
动态多态性是指在运行时才确定调用的对象与方法。通过**继承 + 方法重写(Override)**实现。调用时,基类引用指向子类对象,运行的是子类的方法。
运算符重载
在 C# 中,运算符重载是一种允许开发者为自定义类型(如结构体或类)定义特定运算符行为的功能。这在处理数学实体(如复数、向量、矩阵等)时尤为有用,使得代码更具可读性和直观性。
public struct Vector2D {
public double X, Y;
public Vector2D(double x, double y) {
X = x;
Y = y;
}
public static Vector2D operator +(Vector2D a, Vector2D b) {
return new Vector2D(a.X + b.X, a.Y + b.Y);
}
}只有部分运算符支持重载,比如:
+, -, *, /, %==, !=, <, >, <=, >=在一般的业务应用开发中,运算符重载并不常见,因为其使用可能增加代码的复杂性和理解难度。
命名空间
命名空间的作用
- 组织代码结构。命名空间可以将相关的类、接口、结构、枚举等组织在一起,使代码结构清晰、易于管理。
- 避免命名冲突。多个类可能会有相同的名称,为了避免冲突,可以通过命名空间加以区分。
- 提供访问控制的粒度。虽然命名空间本身不提供访问权限控制,但结合 internal 访问修饰符,可以控制某些类型只在同一个程序集(namespace 通常用来表示模块/组件)中访问。
- 与程序集、目录结构保持一致。在大型项目中,命名空间一般与目录结构对应,便于查找和维护。
项目结构:
MyApp/
└── Services/
└── EmailService.cs
EmailService.cs 中的命名空间:
namespace MyApp.Services- 支持 using 简化调用。使用 using 指令可以简化命名空间前缀,提高可读性。
定义命名空间。
namespace namespace_name{
}使用命名空间
using System;using和using staic的区别
using是导入命名空间,如using System是告诉编译器会用到System命名空间下的类。using static是导入静态成员,如using static System.DateTime;是把DateTime这个类里的静态成员直接带进作用域,使用时可以省略掉类名如:
var now = Now; // 相当于 DateTime.Now
var today = Today; // 相当于 DateTime.Today- C#还提供了
globel关键字,用于全局引入,所有文件都能用,不用每个文件重复写。
global using System;
global using static System.Math;枚举Enum
在 C# 中,枚举(enum)是一种值类型,用于定义一组命名的整型常量,使代码更具可读性和可维护性。
enum Days
{
Sunday, // 默认值为 0
Monday, // 默认值为 1
Tuesday, // 默认值为 2
Wednesday, // 默认值为 3
Thursday, // 默认值为 4
Friday, // 默认值为 5
Saturday // 默认值为 6
}访问修饰符
| 修饰符 | 谁能访问 | 常用位置 |
|---|---|---|
| public | 任何地方 | 类、方法、字段 |
| internal | 同项目内 | 类、方法、字段 |
| private | 类内部 | 字段、方法 |
| protected | 自己+子类 | 继承相关 |
| protected internal | 同项目或子类 | 特殊情况 |
| private protected | 自己或子类,但必须同项目 | C# 7.2之后 |
数据类型
| 类型分类 | 关键字 | .NET 类型 | 占用空间 | 取值范围或特点 |
|---|---|---|---|---|
| 整数类型 | byte | System.Byte | 1字节(8位) | 0 ~ 255 (无符号) |
| sbyte | System.SByte | 1字节(8位) | -128 ~ 127 (有符号) | |
| short | System.Int16 | 2字节(16位) | -32,768 ~ 32,767 | |
| ushort | System.UInt16 | 2字节(16位) | 0 ~ 65,535 | |
| int | System.Int32 | 4字节(32位) | -21亿 ~ 21亿 | |
| uint | System.UInt32 | 4字节(32位) | 0 ~ 42亿 | |
| long | System.Int64 | 8字节(64位) | 超大范围 (有符号) | |
| ulong | System.UInt64 | 8字节(64位) | 超大范围 (无符号) | |
| 浮点类型 | float | System.Single | 4字节(32位) | 单精度浮点数 |
| double | System.Double | 8字节(64位) | 双精度浮点数 | |
| decimal | System.Decimal | 16字节(128位) | 高精度,适合财务运算,值后面带m。 | |
| 字符类型 | char | System.Char | 2字节(16位) | 单个Unicode字符 |
| 布尔类型 | bool | System.Boolean | 1字节(8位) | true / false |
| 字符串类型 | string | System.String | 不定长 | 字符串 |
| 对象类型 | object | System.Object | 不定长 | 所有类型的基类 |
- 其中string和object是引用类型,但使用频率高,常当作基础类型使用。
- var关键字是自动推断类型(编译时确定类型)。
var x = 123; // 推断为 int
x = "hello"; // 编译报错:不能把string赋给int- dynamic关键字是自动推断类型(运行时确定类型,编译时不检查)
dynamic x = 123; // 当前是 int,会自动装箱为object。
x = "hello"; // 没问题,运行时才判断类型
x.DoesNotExist(); // 编译不报错,运行时报错:方法不存在- System.Object是C#中所有数据类型的终极基类。
为什么在代码中使用string而不是String?
string其实是System.String的别名,string是C#中的语法糖。类似地,int是Int32的别名,int也是C#中的语法糖。使用那种方式取决于编码风格,定义变量时推荐使用string,调用方法时推荐使用String。
C#中的逐字字符串
在 C# 里,@ 符号用在字符串前面,表示这是一个 逐字字符串(verbatim string literal),它的主要作用是:
- 忽略转义字符
string path = "C:\\Users\\Tom\\Desktop";
//等价于
string path = @"C:\Users\Tom\Desktop";- 保留字符串里的换行、格式
//错误
string text = "line1
line2";
//正确
string text = @"line1
line2
line3";- 可以和双引号
"共存,@字符串里要写双引号,只需要用两个""。
string msg = @"他说:""Hello World!""";可空类型Nullable
C#中可以使用?表示可空类型Nullable。
int? a=1;
//等价于
Nullable<int> a=1;Null 合并运算符( ?? )
C# 的 Null 合并运算符(??)非常实用,主要是用来简化判断“如果是 null 就用默认值”的逻辑。
变量A ?? 变量B
//如果 `A` 不为 `null`,结果就是 `A`。
//如果 `A` 为 `null`,结果就是 `B`。
//传统写法
string name;
if(name == null)
{
name = "默认值";
}
//??运算符写法
string name;
name=name??"默认值";指针类型
C#中支持指针类型,其功能类似于C/C++的指针。使用指针类型有一定限制,不是常规开发用法。
- 指针类型只能在unsafe环境下使用,编译时需要给添加上
/unsafe选项。
csc /unsafe Program.cs- 只能在非托管代码中用(比如操作内存、数组、性能优化场景),不能直接指向托管对象(类的实例),一般只用于结构体(
struct)或基础数据类型(int、float、char等)。
using System;
class Program
{
unsafe static void Main()
{
int a = 10;
int* p = &a;
Console.WriteLine((int)p); // 输出a的内存地址
Console.WriteLine(*p); // 取值
}
}类型转换
C#支持对数据类型进行隐式转换、显式转换。
隐式转换
隐式转换是指将数据从一个小范围类型转换成大范围类型且不丢失精度,这个过程C#会自动完成,比如int类型数据转换为long类型数据,派生类转换为基类。
int e=123;
long f=e;显式转换
显式转换是指将一个大范围类型数据转换成小范围类型数据,或将一个对象转换成另外一个对象。
显式转换有3种常用方式:
- 强制转换
- 通过Convert、Parse、TryParse方法转换
Covert.toString- 自定义类型的显式转换和隐式转换
强制转换
long a=123;
int b=(int)a;通过Convert转换
Convert是System下的类,其提供了一些静态方法,可以将数据转换为常用的基础数据类型。
long f = e;
double g = Convert.ToDouble(f); ;通过Parse、TryParse将string解析成基础数据类型
基础数据类型会带有静态方法Parse、TryParse。
- Parse方法可以将字符串解析成基础数据类型,如解析失败则抛出异常。
- TryParse方法可以将字符串解析成基础数据类型,返回一个bool值表示解析成功or失败。
double num = double.Parse(numStr);
bool ifParseSucc=double.TryParse(numStr,out num);自定义类型的显式转换和隐式转换
在C#中,自定义类型的显式转换和隐式转换,通过implicit(隐式转换)和explicit(显式转换)关键字完成。
//自定义
public class Money
{
public decimal Amount { get; set; }
public Money(decimal amount)
{
Amount = amount;
}
// 隐式转换:Money -> decimal
public static implicit operator decimal(Money money)
{
return money.Amount;
}
// 显式转换:decimal -> Money
public static explicit operator Money(decimal amount)
{
return new Money(amount);
}
}
//使用
Money m = new Money(100.5m);
// 隐式转换
decimal d = m;
// 显式转换
Money m2 = (Money)200.75m;隐式转换和显式转换都可以达到数据类型转换的效果,那什么时候该定义为隐式转换,什么时候该定义为显式转换呢?
根据官方规范参考(微软建议),仅在转换绝对安全且不会丢失信息时使用implicit隐式,否则使用explicit显式。
程序逻辑
定义变量
int a=1;
int b;
b=2;定义静态变量
c#中使用static关键字定义类的静态变量,静态变量可以在整个类中可见。
class MyClass{
static int a=1;
}定义常量
c#中使用const关键字定义常量,常量定义后无法被修改。
const int a=1;
const string str="hello world";运算符
c#支持以下几类运算符:
| 类型 | 运算符 |
|---|---|
| 算术运算符 | + - * % ++ -- |
| 关系运算符 | == != > < >= <= |
| 逻辑运算符 | && || ! |
| 位运算符 | & | ^ ~ << >> |
| 赋值运算符 | = += -= *= /= %= <<= >>= &= ^= |= |
| 其他运算符 | sizeof() typeof() &a(返回变量的地址) *a(指向一个变量的指针) is(判断对象是否为某一类型) as(强制转换,即使转换失败也不会抛出异常) ??(Null 合并运算符) |
运算符优先级:有括号先括号,后乘除在加减,然后位移再关系,逻辑完后条件,最后一个逗号,。 |
分支
if(a==b){
do something;
}
else if(c==d){
do other thing;
}
else{
do other thing;
}循环
while(a==b){
repeat something;
}
do{
repeat something;
}while(a==b)
for(int i=0;i<n;i++){
repeat something;
}方法
class MyClass{
public int test(int i){
return i+1;
}
}out关键字
out用来告诉编译器——这个参数是用来回传数据的,不是传进去用的,而是方法里赋值,方法外拿结果。
//定义
class MyClass{
public static void test(out int i){
i=1; //必须对out赋值
}
}
//使用
int a;
MyClass.test(a) //a将会被赋值为1ref关键字
ref关键字对变量的内存地址的引用,在方法内访问的变量和方法外访问的变量是同一个,他们具有相同内存地址。
class MyClass{
public static void test(ref int i){
i=1;
}
}
//使用
int a=2;//需要先定义,有值才会有内存地址
MyClass.test(a) //a将会被赋值为1out关键字与ref关键字的区别?
| 区别点 | ref | out |
|---|---|---|
| 调用前是否需要赋值 | 需要 | 不需要 |
| 方法内是否必须赋值 | 可以不赋值 | 必须赋值 |
异常处理
基本结构:
try
{
// 尝试执行的代码块,可能抛出异常
}
catch (ExceptionType1 ex)
{
// 捕获特定类型的异常
}
catch (Exception ex)
{
// 捕获其他异常
}
finally
{
// 无论是否发生异常,都会执行的代码块(通常用于释放资源)
}常见异常类的继承层级关系
System.Object
└── System.Exception
├── System.SystemException // 系统异常基类
│ ├── System.NullReferenceException
│ ├── System.IndexOutOfRangeException
│ ├── System.DivideByZeroException
│ ├── System.InvalidOperationException
│ ├── System.ArgumentException
│ │ ├── System.ArgumentNullException
│ │ └── System.ArgumentOutOfRangeException
│ └── System.IO.IOException
│ ├── System.IO.FileNotFoundException
│ └── System.IO.DirectoryNotFoundException
└── System.ApplicationException // 应用程序异常(可用于自定义异常)数组
int[] nums;
int[] nums={1,2,3};
int[] nums=new int[10];
int[] nums=new int[4]{1,2,3,4};
int[] nums=new int[]{1,2,3,4};集合
常用的集合类定义在System.Collection、System.Collections.Generic、System.Collections.Concurrent命名空间下,包括:
- 非泛型集合(System.Collections):适用于早期.NET版本,不推荐在新项目中使用(性能较差,类型不安全)
| 类名 | 特点 |
|---|---|
ArrayList | 动态数组,存储 object 类型,容量可变。 |
Hashtable | 键值对集合,键和值都是 object 类型。 |
Queue | 先进先出(FIFO)队列。 |
Stack | 后进先出(LIFO)栈。 |
SortedList | 按键排序的键值对集合。 |
BitArray | 用于表示一组位(bit)值的集合 |
- 泛型集合(System.Collections.Generic):性能好,类型安全,是推荐使用的主流集合类
| 类名 | 特点 |
|---|---|
List<T> | 动态数组,使用最广泛,适用于大多数顺序存储场景。 |
Dictionary<TKey, TValue> | 哈希表结构的键值对集合,查找快。 |
HashSet<T> | 无重复元素集合,内部基于哈希表。 |
SortedList<TKey, TValue> | 有序键值对集合,键排序,查找快,占用内存小。 |
SortedDictionary<TKey, TValue> | 有序键值对集合,基于红黑树,插入删除快。 |
Queue<T> | 泛型先进先出队列。 |
Stack<T> | 泛型后进先出栈。 |
LinkedList<T> | 双向链表,适合频繁插入/删除操作的场景。 |
ObservableCollection<T> | 支持事件通知的集合(用于UI数据绑定,如WPF)。 |
- 线程安全集合(System.Collections.Concurrent):适用于多线程环境
| 类名 | 特点 |
|---|---|
ConcurrentDictionary<TKey, TValue> | 线程安全的字典。 |
ConcurrentQueue<T> | 线程安全的队列。 |
ConcurrentStack<T> | 线程安全的栈。 |
ConcurrentBag<T> | 无序线程安全集合,适合并发添加元素。 |
BlockingCollection<T> | 支持阻塞和限界的集合,用于生产者-消费者模式。 |
List<string> names = ["<name>", "Ana", "Felipe"];
foreach (var name in names) {
Console.WriteLine($"Hello {name.ToUpper()}!");
}I/O流
在 C# 中,“流”(Stream)是指用于读写数据的一种抽象概念,广泛应用于文件操作、网络通信、内存读写等场景。C# 提供了一整套基于 System.IO 命名空间的流类体系,来支持各种不同类型的数据传输。
常用的流类型
| 类型 | 说明 |
|---|---|
FileStream | 读取/写入文件中的字节 |
MemoryStream | 读写内存中的数据,适合中间处理 |
BufferedStream | 为其他流添加缓冲功能,提高效率 |
NetworkStream | 用于网络数据传输 |
CryptoStream | 与加密算法结合的数据流 |
StreamReader / StreamWriter | 专门处理文本内容的流 |
BinaryReader / BinaryWriter | 处理二进制数据的高层封装 |
预处理指令
在 C# 中,**预处理指令(Preprocessor Directives)**是以 # 开头的指令,用于在编译之前告诉编译器如何处理代码。这些指令不是 C# 语言的一部分,而是由编译器在编译之前解释执行的,主要用于条件编译、调试控制等目的。
常见的预处理指令如下:
| 指令 | 含义 |
|---|---|
#define | 定义一个符号 |
#undef | 取消一个符号的定义 |
#if | 如果符号被定义,则编译后面的代码块 |
#elif | 如果前面的条件为 false,检查这个条件 |
#else | 所有条件都不满足时执行 |
#endif | 结束条件编译块 |
#warning | 编译时生成一个警告信息 |
#error | 编译时生成一个错误信息,阻止编译 |
#region | 定义一个可折叠的代码区域(用于代码整理) |
#endregion | 结束一个 #region 区域 |
#pragma | 控制编译器的行为(如忽略特定警告) |
注意事项
预处理指令一般用于条件编译。过多使用预处理指令会降低代码可读性。
示例
如下示例展示了预处理指令的条件编译。通过定义不同的符号实现生产、开发环境的编译。
#define DEBUG
using System;
class Program
{
static void Main()
{
#if DEBUG
Console.WriteLine("Debug 模式");
#else
Console.WriteLine("Release 模式");
#endif
}
}特性
在 C# 中,特性(Attribute)是一种用于向程序的元数据添加声明性信息的机制。你可以把特性看作是“标签”或“注解”,它们不会直接影响程序的运行逻辑,但可以在运行时或编译时被反射机制读取,用来控制行为或传递信息。
常用内置特性
| 特性名 | 说明 |
|---|---|
[Obsolete] | 标记为过时 |
[Serializable] | 表示可序列化 |
[DllImport] | 用于调用非托管代码 |
[NonSerialized] | 防止字段被序列化 |
[AttributeUsage] | 控制自定义特性的用法 |
特性的基本语法结构
[AttributeName(positional_arg1, positional_arg2, ..., NamedArg1 = value1, NamedArg2 = value2)]特性的参数分为两类:
- 位置参数(Positional Parameters):必填参数,在构造函数中定义。位置参数只能在特性的构造函数中定义赋值,赋值时参数顺序要一致,且必须提供值。
- 命名参数(Named Parameters):可选参数,设置为属性或字段。赋值时以
key=value的形式赋值,不需要定义构造函数内,但需要定义get/set函数。
自定义特性
[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute
{
public string Message { get; }
public LogAttribute(string message)
{
Message = message;
}
}- 定义一个类,继承自Attribute。
- 使用AttributeUsage特性,声明自定义特性的应用范围和属性。
- 声明位置参数,定义构造函数。
- 可选声明命名参数,定义set/get方法。
- 使用自定义特性。
[LogAttribute("执行了此方法")]
public void DoWork()
{
Console.WriteLine("工作中...");
}反射
C# 的 反射(Reflection) 是 .NET 提供的一种强大机制,它允许程序在运行时动态地检查、访问、修改类型的信息,比如类、接口、方法、属性、字段、构造函数等,甚至可以动态地创建对象和调用方法。
反射的基本用法
反射类在System.Reflection命名空间下。
核心类Type的获取方式
Type type=typeof(MyClass);
Type type=obj.getType();通过反射获取对象信息
Type type=obj.getType();
Console.WriteLine("类名:" + type.FullName);
Console.WriteLine("是否为类:" + type.IsClass);
foreach (var method in type.GetMethods())
{
Console.WriteLine("方法:" + method.Name);
}通过反射动态创建对象
Type type = typeof(MyClass);
object instance = Activator.CreateInstance(type);通过反射调用对象方法
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(instance, new object[] { "张三" });通过反射访问/修改对象属性
FieldInfo field = type.GetField("myField", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(instance, 123);
object value = field.GetValue(instance);通过反射获取类特性
[MyCustom]
class MyClass {}
Type type = typeof(MyClass);
object[] attrs = type.GetCustomAttributes(false);
foreach (var attr in attrs)
{
Console.WriteLine(attr.GetType().Name);
}常见用途
| 用途 | 描述 |
|---|---|
| 获取类型信息 | 获取类、接口、结构、枚举等的元数据 |
| 动态创建对象 | 无需编译时确定类型,运行时创建对象 |
| 动态调用方法 | 不知道方法名的情况下,调用对象方法 |
| 读取/修改字段 | 操作私有或公共字段、属性 |
| 获取自定义特性 | 获取 Attribute 信息 |
委托
在 C# 中,委托(Delegate)是一种类型安全的函数指针,可以把方法作为参数进行传递,或者将方法赋值给变量。它是实现事件机制、回调函数等功能的基础。它允许你引用一个方法,并通过引用调用方法。
使用自定义委托
- 使用委托之前需要先定义一个委托类型,委托必须与目标方法的签名一致,指入参和出参。
- 还需要有一个目标方法
- 实例化委托,可以使用new或不使用new。
- 通过委托调用目标方法。
// 声明一个委托类型(必须与方法的签名一致)
delegate int MyDelegate(int x, int y);
//目标方法
int add(int n1,int n2){
return n1+n2;
}
// 使用委托
MyDelegate del = Add;
int result = del(3, 4); // 相当于调用 Add(3, 4)使用场景
| 场景 | 描述 |
|---|---|
| 事件处理 | 比如按钮点击,使用委托注册回调函数 |
| 回调机制 | 方法执行完之后通知调用方 |
| LINQ表达式 | 委托作为参数(如 Func<T>) |
| 多播委托 | 同时调用多个方法 |
多播委托
委托可以通过+实现连接委托、-移除委托,形成一个委托列表,这被称为委托多播或组播。
参数会经过委托列表传递,上一个方法执行的结果会传递到下一个方法的入参。
delegate void MyDelegate();
void Method1() => Console.WriteLine("方法1执行");
void Method2() => Console.WriteLine("方法2执行");
MyDelegate del = Method1;
del += Method2;
del(); // 输出:方法1执行\n方法2执行C#内置的委托
Action:无返回值的方法(最多16个参数)
// 无参数
Action sayHello = () => Console.WriteLine("Hello!");
sayHello();
// 有参数
Action<string> greet = (name) => Console.WriteLine("Hello, " + name);
greet("Evan")Func<TResult>:有返回值的方法
Func<int> getNumber = () => 42;
Console.WriteLine(getNumber(2, 3)); // 输出 42
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3)); // 输出 5Predicate<T>:返回布尔值的方法
Predicate<int> isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // 输出 True事件
在 C# 中,事件(event)是一种用于实现发布-订阅模式(或观察者模式)的机制,它允许对象向其他对象发送通知,通常用于响应用户操作或系统状态变化。
- 使用event关键字基于委托定义事件
- 事件和委托都定义在发布者类中,事件由发布者类触发
- 订阅者实现事件订阅和基于委托签名实现事件处理逻辑。
//发布者
public class Publisher
{
// 1. 定义一个委托类型
public delegate void NotifyHandler(string message);
// 2. 定义一个事件,基于上述委托
public event NotifyHandler OnNotify;
//触发事件
public void DoSomething()
{
Console.WriteLine("Publisher: 正在做一些事情...");
// 3. 触发事件(如果有订阅者)
OnNotify?.Invoke("任务完成!");
}
}
//订阅者
public class Subscriber
{
public void HandleNotification(string message)
{
Console.WriteLine($"Subscriber 收到通知: {message}");
}
}
//业务类,创建发布者和订阅者,订阅者订阅事件,发布者触发事件。
public class Program
{
public static void Main()
{
var pub = new Publisher();
var sub = new Subscriber();
// 4. 订阅事件
pub.OnNotify += sub.HandleNotification;
pub.DoSomething(); // 会触发事件
}
}泛型
C# 的泛型(Generics)是 .NET 中一个非常强大的特性,它允许你在编写类、接口、方法、委托等时使用占位类型,在实际使用时再指定具体的数据类型。这样做可以提高代码的重用性、类型安全性和性能。
基本用法
泛型类
public class MyList<T>
{
private T[] items;
public void Add(T item) { /*...*/ }
}
//添加泛型的约束范围
public class Comparer<T> where T : IComparable<T> { }泛型方法
public void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}泛型接口
public interface IRepository<T>
{
void Add(T entity);
}泛型委托
delegate T change<T>(T n);匿名函数
在 C# 中,匿名方法(Anonymous Method)是一种不需要命名的方法,通常用于定义委托的内联实现。它让你可以在声明委托变量时直接赋值一个方法体,而不需要额外定义一个具名方法。
匿名函数有两种形式:
- lambda表达式
- 匿名方法
lambda表达式
语法:
(参数列表) => 表达式或语句块
()=>Console.writeLine("hello world");
x=>x*x;
(x,y)=>x+y;
(x,y)=>{
var z=x+y;
return z;
}匿名方法
匿名方法(Anonymous Method)是 C# 2.0 引入的一种语法,它允许你在定义委托时不单独声明一个方法名,而是直接内联一个方法体。
语法:
delegate(参数列表) { 方法体 }
Func<int, int, int> add = delegate(int a, int b)
{
return a + b;
};
Console.WriteLine(add(3, 4)); // 输出 7什么时候用匿名方法?
通常现在更推荐使用 lambda 表达式,因为它更简洁、灵活。
但在某些场景下,如果你觉得 delegate 更直观、明确(比如老代码或想保留结构化语句块),也可以使用匿名方法。
多线程
在C#中,实现多线程有几种方式:
使用Thread类
Thread类可以管理线程,适合少量控制线程,执行简单任务时使用
using System;
using System.Threading;
void MyTask()
{
Console.WriteLine("线程开始执行");
}
Thread t = new Thread(new ThreadStart(MyTask));
t.Start();使用线程池ThreadPool
ThreadPool适合在有大量简单任务时使用,但无法设置线程名称/优先级。
using System;
using System.Threading;
class Program
{
static void Main()
{
ThreadPool.QueueUserWorkItem(PrintInfo);
Console.WriteLine("主线程继续运行");
Thread.Sleep(1000); // 等待任务完成
}
static void PrintInfo(object state)
{
Console.WriteLine("在线程池中执行任务");
}
}Task类
Task类是比较推荐的使用方法,适合用于大多数后台或计算任务,可异步并发、可取消/等待/获取结果的任务,但不适合重cpu的任务。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task<int> task = Task.Run(() =>
{
return 1 + 2;
});
int result = await task;
Console.WriteLine($"结果:{result}");
}
}Parallel并行库
Parallel库适合处理大型数组或列表,或CPU 密集型任务(图像处理、模拟等)
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Parallel.For(0, 5, i =>
{
Console.WriteLine($"任务 {i} 正在执行");
});
}
}包管理器器
- NuGet