您提供的信息似乎不完整,无法确定您想要表达的具体内容。如果您能提供更多的上下文或者详细说明,我会尽力帮助您。例如,如果您是在询问某个产品、服务或者是在描述某种情况,请提供更多的信息。
摘要:C#九成九新个人用入门指南 前言 如果你是第一次学习编程,那么,可能会非常困难,如果你曾经学过面向对象的编程语言,那么可能会非常轻松 C:你干脆直接提我名字得了吧 在我们正式学习之前,我需要讲几个比较基础的知识 1. 高级语言,低级语言,强
C#九成九新个人用入门指南
前言
如果你是第一次学习编程,那么,可能会非常困难,如果你曾经学过面向对象的编程语言,那么可能会非常轻松
C:你干脆直接提我名字得了吧
在我们正式学习之前,我需要讲几个比较基础的知识
1. 高级语言,低级语言,强类型,弱类型
高级语言和低级语言,这两个概念对初学者来会非常头疼,网上的程序对这几个概念天天吵,吵得头都大了
但是当你学习一两年之后,你会发现,这两个概念毫无卵用,
当然,这样的概念还不少,例如强类型语言和弱类型语言,
一群人吵来吵去,结果别人官方压根就没有定义过这东西
别管什么高级语言,低级语言,强类型,弱类型,能写出好程序的编程语言才是好语言,别纠结那么多
2. C与C# 执行方式的不同
C:通过编译器将程序转换为机器指令,然后程序运行时直接运行机器指令
// 下面是GCC编译器实现编程的过程(源文件到可执行文件经历那几个步骤)
// 预处理->编译->汇编->链接
// 1.预处理: 展开头文件、删除掉注释、定义的宏进行替换、条件编译处理
gcc -E -> .i 代码文件(.i文件)
-------------------------------------------------------------------
// 2.编译: 将C语言文件变成汇编语言文件
gcc -S -> .s 汇编语言文件(.s文件)
-------------------------------------------------------------------
// 3.汇编: 将汇编语言文件变成二进制文件
gcc -c -> .o 目标文件(.o文件)
-------------------------------------------------------------------
// 4.链接: -> .out 可执行文件(.out文件)
gcc -o
C#
先通过编译器将程序转换为IL中间语言(即Intermediate Language,微软.NET平台中的中间语言)
在运行时,例如.NET平台中CLA会动态的将IL中间语言转换为机器指令,最后执行机器指令
C#程序 ->IL中间语言 ->机器语言
一.基础语法(略讲)
毕竟大部分编程语言大差不差,别浪费太多时间在这里
1. Hello World(C#基础结构)
新上手一门编程语言,干的第一件事不是去看语法
而是先写一个HelloWorld(学习新的编程语言的编译方式以及代码结构)
我虽然不精通任何一门编程语言,但是我精通多门语言的HelloWorld
// 1.命名空间引用 -> 输入输出流
using System;
// 2.命名空间 -> namespace
namespace HelloWorldCS
{
// 3.类 -> class
internal class Program
{
// 4.函数(方法)
static void Main(string[] args)
{
// 5.这里是程序入口
// 6.打印函数
Console.WriteLine("Hello World!");
// 7.注释
// Console.WriteLine();
// 8.C#不需要return 0;作为结束
}
}
}
// =====================================================
// 但实际上,C#的最小结构这样就可以了
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello World");
}
}
当然,我们可以用C和C++对比一下
// C
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
// =========================================================
// C++
#include <iostream>
int main() {
std::cout << "Hello World" << std::endl;
return 0;
}
// =========================================================
// C#
using System;
class Program {
static void Main() {
Console.WriteLine("Hello World");
}
}
2. 数据类型
2025-11-19 [新增]
-----------------------------------------------------
# C#中,一般分为值类型, 引用类型, 指针类型 这三种数据类型
-----------------------------------------------------
值类型:
1.数值类型 -> int, float, double, decimal等
2.结构类型 -> 结构体
3.枚举类型 -> 枚举体
-----------------------------------------------------
引用类型:
1.数组 -> int[] arr
2.字符串 -> string s
3.类 -> class MyClass { ... }
4.接口 -> interface IMyInterface { ... }
5.委托 -> delegate void MyDelegate();
1.基本数据类型 -> 除了以下bool和char,几乎和C/C++没什么区别,那就偷懒不写了
[!TIP]
❗⭐ C 和C++开发人员请注意,在 C# bool 中,不可转换为 int
1.C# 中禁止bool类型的隐式转换和算术运算
2.⭐C#中char 是 System.Char 的别名,占两个字节,用来表示、存储一个 Unicode 字符
3.初始化变量必须赋值,C和C++可能会允许你使用,但是C#编译器过都不会让你过
这里需要纠正一下,准确的说,应该是:
C# 使用变量前必须先赋值,C / C++可能允许使用,
但是C#中,使用未赋值变量编译器会直接报错,如果没有赋值就使用编译器会警告但是不会报错
static void TM()
{
int num;
bool b;
string s;
Console.WriteLine("111"); // 这里可以正常编译和执行
// Console.WriteLine(num); // 这里未赋值就使用,编译不通过
// 除去变量num,其它均声明但未赋值且没有使用,编译器会显示以下警告内容
// warning CS0168: 声明了变量“b”,但从未使用过
// warning CS0168: 声明了变量“s”,但从未使用过
}
2.复合数据类型
类型分类
C++
C#
说明
数组
int arr[10]
int[] arr = new int[10];
C++ 和C#数组均是固定大小数组
C# 数组是特殊的引用类型,允许直接在栈上分配
一旦长度固定,油门焊得得比想象中的还死
甚至无法扩容
std::array<int,10>
(无直接等价)
固定长度值类型数组
动态数组
var list = new List<int>();
能扩容,但是底层仍然是 int[],
只是不断创建更大的数组来换胎
你看到的是动态行为,但本质不是数组扩容行为
字符串
char*
string
C++ 裸指针字符串,需手动管理内存
const char*
string
字面量字符串,对应 C# 字符串常量
std::string
string
C++ 可变;C# 不可变
键值对
std::pair<K,V>
KeyValuePair<K,V>
键值对容器
字典
std::map<K,V>
SortedDictionary<K,V>
有序字典
std::unordered_map<K,V>
Dictionary<K,V>
哈希字典(无序)
集合
std::set<T>
SortedSet<T>
有序集合
std::unordered_set<T>
HashSet<T>
无序集合(哈希)
动态数组
std::vector<int>
List<int>
动态数组,支持自动扩容
链表
std::list<T>
LinkedList<T>
双向链表
队列
std::queue<T>
Queue<T>
队列
栈
std::stack<T>
Stack<T>
栈
3.面向对象数据类型
类型分类
C++ 类型
C# 类型
说明
类
class MyClass
class MyClass
均为面向对象类定义
C++ 默认私有继承成员
结构体
struct MyStruct
struct MyStruct
C++ 默认 public;C# 默认值类型
接口
纯虚类
class IFoo { virtual void f()=0; };
interface IFoo { void f(); }
C++ 模拟接口需手动实现虚析构
C# 有语言级支持
枚举
enum MyEnum
enum MyEnum
基本枚举;C# 默认底层类型为 int
enum class MyEnum
(无直接等价)
C++11 作用域枚举(强类型)
委托
std::function<void()>
(C++没有委托,但函数包装器和C#委托类似)
delegate void MyDelegate()
可调用对象封装
函数指针
void(*fp)(int)
Action<int> / Func<T>
C++ 裸指针函数
C# 委托封装函数引用
[!NOTE]
std::function 可封装 lambda、函数指针、仿函数;C# delegate 同理,但更受 CLR 支持
C# struct 是值类型(分配在栈或内联),C++ 结构体和类几乎等价(仅默认访问修饰符不同)
3. ⭐数据类型的转换
2025-11-19[新增]
------------------------------------------------------------------------------
C#和C/C++中的数据转换存在着非常大的区别
C/C++中几乎什么数据类型都可以转换
但是C#中对此非常严格, 我旁边的同事甚至对我说,C和C++简直太可怕了(我:?????)
这里只讲强类型转换,强制性数据类型都明白了,隐式转换也差不多明白了
强制数据类型转换
(1)C 强制性数据类型转换
C 的 强制数据类型转换 是直接根据内存 去重新解释
也就是说本质是:我不管你是上面类型,我只按新的类型格式去解释那块内存
/* C */
float f = 3.14;
char* p = (char*)&f; // 我只要地址,随便转
char buffer[32];
int a = (int)buffer; // 别管正不正确,反正就是要强行操作
(2)C#强制性数据类型转换
C# 的 强制数据类型转换 是语义转换
C# 把其当成一种“合法的类型变换”,必须存在明确规则
也就是说:必须有 "语言允许的转换" 或 "类型提供的转换运算符"
// 最浅显的例子就是
float num = 1;
string str = (string)num;
// 甚至不允许你char和int类型相互转换
int num = 1;
char str = num;
// C# 会觉得上面的转换都没有意义,更没有安全性,拒绝转换,没错,它罢工了
那么,问题来了?什么情况下,C#会允许强制数据类型转换?
你要画什么大饼,才可以让他开工呢?可以去问问老板,毕竟是画饼专业户
你说太多了,记不住怎么办,肯定随用随查,用多了就记住了,
下面是我查到的一部分,但是肯定也不止这一些用法,当然查到的也不代表就使用的很多,仅作参考
/* 1.数值类型 */
int a = (int)3.5f; // 浮点数转整数,弄丢小数部分
------------------------------------------------------------------------------
/* 2.有继承关系的类 */
object o = "abc";
string s = (string)o; // 上下文必须兼容,编译时检查继承/实现关系
------------------------------------------------------------------------------
/* 3.装箱与拆箱 */
object o = 123;
int x = (int)o; // 拆箱必须类型完全匹配,否则抛异常
------------------------------------------------------------------------------
/* 4.Convert 类方法 */
// System.Convert 提供一系列通用方法,可在数值类型、字符串、布尔等之间转换,失败会抛异常
// 这里再特别提醒一下C/C++开发人员 -> C#中bool类型就是bool类型,别直接当成0和1计算了
string str = "123";
int i = Convert.ToInt32(str);
double d = Convert.ToDouble(123); // int → double
bool b = Convert.ToBoolean(1); // 1 → true
------------------------------------------------------------------------------
/* 5.Parse 或者 TryParse */
// 字符串 -> 数值类型,常用在用户输入或文件解析场景
// Parse 失败抛异常
// TryParse 返回 bool,不抛异常
string s = "123";
int n = int.Parse(s); // 成功则 n=123
bool success = int.TryParse("abc", out int result); // false,不抛异常
------------------------------------------------------------------------------
/* 6.as 操作符(仅引用类型) */
// 用于 安全类型转换,返回 null 而不是抛异常(当转换失败时)
// 只能用于引用类型或可空类型
object o = "hello";
string s = o as string; // 成功则 s="hello"
Button btn = o as Button; // 失败,btn=null
------------------------------------------------------------------------------
/* 7.is + 强制转换(安全组合) */
// 先判断类型,再进行显式转换,避免异常
object o = 123;
if (o is int n) // C# 7+ pattern matching
{
Console.WriteLine(n + 1);
}
if (o is int) // 常规写法
{
int n = (int)o;
}
4. 其他常见关键字(auto, var, ref, out)
没什么差异的和上面出现过的就不写了.JPG
(1)auto VS var
C++
C#
说明
万能推导
auto
var
任意类型推导;C# 的 var 是静态类型
lambda语句
C++11: [](){}
()=>{}
命名空间
using namespace std;
using System;
宏定义
#define
#define
C#宏定义必须放在
所有 using、namespace、class 之前
(2)ref VS out
在清华大学出版社《C#编程从基础到应用》一书中定义:
引用参数 -> ref修饰的参数
输出参数 -> out修饰参数,使用效果类似于return
----------------------------------------------------------
ref
让参数按引用传递,函数可以直接改外部变量的值
即:ref -> 有值带进去,改完带出来
out
专门用来"往外带数据",相当于多了一个"返回值"
即:out -> 空手进去,带着结果出来
ref 是"引用传递",out 是"输出参数"
用 ref 表示"我要读也要写",用 out 表示"我只负责写"
C# 的 ref 像 C++ 的引用; out 像 int* 那种输出指针
1)ref
这个变量是现成的,我只是进去把它改一下(在方法中修改变量本体)
// 不改变外部变量数值
void Test(int x) { x = 100 }
// 改的是外部变量本体
void Increase(ref int x) { x = 100; }
int num = 20;
Test(num);
int value = 5;
Increase(ref value);
Console.WriteLine(num); // 20
Console.WriteLine(value); // 100
感觉和C++中的引用功能更加相似
// C++ 引用
void foo(int& x);
// C# ref
void Foo(ref int x);
2)out
我给你一个空杯子,你负责往里倒牛奶(从方法中返回额外变量)
void Add(int a, int b, out int sum)
{
sum = a + b; // out 参数必须在函数里被赋值
}
int result; // 不需要初始化
Add(3, 5, out result);
Console.WriteLine(result); // 输出 8
功能上,感觉有一些像指针
// C++ 指针输出
bool TryGet(int* outValue)
{
*outValue = 42;
return true;
}
// C# out
bool TryGet(out int value)
{
value = 42;
return true;
}
特性
ref
out
进入方法前必须初始化
是
否
方法内部必须赋值
否
是
主要目的
修改已有数据
返回额外数据
是否需要双向操作
是:要看原值,也要改
否:通常只关心输出
语义
“带着现成的值进来”
“给我变量,我负责塞值进去”
推荐一篇讲的非常好且详细的博客:C#中out和ref之间的区别 - 石shi - 博客园
(3)const和readonly
const:声明 编译时常量,值在编译时就确定,不能改变
修饰符: 必须是 static(隐式静态),不能用 readonly 修饰符叠加
readonly:声明 运行时只读字段,值在运行时可赋值一次(构造函数中)**
修饰符: 可以是实例字段或静态字段,可与 static 叠加
省流:
const可以理解为一种常量,通常情况下值不变;
readonly可以根据英文理解为 read + only = 只读的意思
public class T
{
// 静态字段和只读字段
private readonly string name;
// 只读字段
public const int num = 1;
}
5. 语句
C#中的基本语句和C/C++中的语句功能基本一样,就不过多讲述了,但是C#中的switch比C/C++中的更加强大
(1)switch
// C++中switch禁止使用string,仅能判断整数和枚举
std::string = "start";
switch (string) { ❌
case "start": Console.WriteLine("Run"); break;
case "stop": Console.WriteLine("Halt"); break;
default: Console.WriteLine("Unknown"); break;
}
// =======================================================
// C#中switch支持string使用,包括且不限于字符串,条件判断,模式匹配等
string cmd = "start";
switch (cmd) {
case "start": Console.WriteLine("Run"); break; // 字符串匹配
case "stop": Console.WriteLine("Halt"); break;
case int i when i < 0: Console.WriteLine("negative"); break; // 条件判断
case int i when i < 10: Console.WriteLine("small"); break;
default: Console.WriteLine("Unknown"); break;
}
(2)foreach
2025-11-21[新增]
C#中还有一个比较好用的语句 -> foreach语句,C++中没有,但是C++11之后的for却有类似的功能
foreach语句:用于遍历数组或者对象集合中的元素
// C#
int[] array = new int[] {0, 1, 2, 3};
// 对数组中的元素进行遍历
foreach(int element int array)
{
System.Console.WriteLine(element);
}
// ====================================================
// C++11
#include <vector>
#include <iostream>
using namespace std;
vector<int> v = {0, 1, 2, 3};
for (int x : v)
{
cout << x << endl;
}
但是需要注意的是
// C# 不能这样直接修改数组元素
int[] arr = {1,2,3};
foreach (var x in arr)
{
x = x + 1; // ❌ 编译错误,x是只读副本
}
// 但是C++却可以
vector<int> v = {1,2,3};
for (int &x : v)
{
x += 1; // ✅ 修改容器内的元素
}
小结:
C# foreach : 安全、只读、简洁,但不能修改值类型元素本身
C++11及其之后的 范围 for : 灵活、高效,可直接修改容器元素
二.面向对象编程(略讲)
也是大部分相同,但是却有一定的差异性,主要了解差异性即可
1. 类和对象
类是构成程序的主题,也是现实世界事物的模型
它把数据(字段)与行为(方法)封装在一起
通过实例化类可以在内存中生成对象,也称为实例,对象 = 类的实例
对象是类的具体存在,类则是对象的抽象蓝图
类库:类的仓库,类库引用是使用命名空间的物理基础,类和命名空间放在类库里面
.dll 类库,动态链接库
DLL引用(黑盒引用->无代码,对编译好的DLL的直接引用)
项目引用(白盒引用->有代码,对源代码进行引用)
// 使用new创建实例化对象
Test myTest = new Test();
类的三大成员:属性,方法,事件
事件:类或者对象通知其他类和对象的机制(C#特有 )
静态成员和非静态成员
静态成员:类的成员
非静态成员:可以理解为实例成员,语义上来讲,就是对象的成员
看了一个教程视频,这他喵的在讲什么啊,上帖子!这up讲这么复杂干什么
嵌入式面试题 - C++总结(一) - 想不到ID暂时就这样了 - 博客园
1.定义
普通成员:属于类的对象
静态成员:属于类本身
2.内存分配
普通成员:每个对象创建时分配内存,实例化时存在
静态成员:类加载时分配内存,所有对象共享同一地址
3.生命周期
普通成员:与对象生命周期相同,实例化时(类创建对象时)创建,销毁时释放
静态成员:与程序生命周期相同,类加载时初始化,程序退出时销毁
4.访问权限
普通成员:可以访问对象的普通成员和静态成员
静态成员:只能访问静态成员,不能访问普通成员
5.初始化
普通成员:在类中(构造函数)初始化
静态成员:必须在类外进行初始化
2. 继承,封装和多态
1.封装没什么好讲的,区别大概只有以下几点
- 默认权限相同:类成员 private,结构体 public
- C#无友元friend
- C#拥有GC回收机制
==========================================================
2.继承
C#中类不能有多个基类,禁止多继承
但是类可以多继承接口
子类 继承 父类和接口时,必须全部实现继承类或接口中的函数
没想到我也有怀念虚继承和纯虚函数的一天
class A { }
class B : A { } // ✅ 单继承
class C : A, B { } // ❌ C#中不允许
3.多态
对比点
C++
C#
多态实现机制
虚函数表(vtable)
CLR 方法表(相似于 vtable)
关键字
virtual / override / final
virtual / override / sealed / new
默认函数类型
非虚函数
非虚函数
接口机制
通过纯虚类实现接口
原生支持接口
运行环境
原生机器码执行
CLR 托管环境(JIT 编译)
内存管理
手动(需 delete)
自动垃圾回收 (GC)
多继承
支持(需虚继承解决菱形问题)
不支持(仅单继承 + 多接口)
运行时绑定
依赖虚表指针 (vptr)
由 CLR 维护方法分派表
类型安全
相对弱(可转为无效指针)
强类型检查(运行时验证)
调用方式
基类指针/引用指向派生类对象
基类引用指向派生类对象
三.一些重要的语法(详讲)
这才是需要我们去花时间学习和整理的部分
1. 一些操作符
?: 三目运算符 // x > 0 ? "正" : "负"
?? 空合并运算符 // a ?? b 等同于 (a != null) ? a : b
?. 空条件运算符 // 如果 a 不为 null,则访问 a.b;否则返回 null
Person p = null;
Console.WriteLine(p?.Name); // 如果p为空,不会抛异常,只返回 null; 反之访问p.Name
// 相当于if (p != null) Console.WriteLine(p.Name);
/* ———————————————————————————————————————————————————————————————————————————————————————————— */
// 位运算符
<<= 左移且赋值运算符 // C <<= 2 等同于 C = C << 2
>>= 右移且赋值运算符 // C >>= 2 等同于 C = C >> 2
&= 按位与且赋值运算符 // C &= 2 等同于 C = C & 2
^= 按位异或且赋值运算符 // C ^= 2 等同于 C = C ^ 2
|= 按位或且赋值运算符 // C |= 2 等同于 C = C | 2
/* ———————————————————————————————————————————————————————————————————————————————————————————— */
// Lambda运算符
=> Lambda 运算符
/* ———————————————————————————————————————————————————————————————————————————————————————————— */
// 字符串插值
$
/* $这个语法我单独写在这里,但实际上也非常简单,某种程度上可以看作QT - C++中的断言 */
// $"..." 的作用就是让你能在字符串里直接写 {变量},C# 会自动替换成对应的值
// 输出 -> 我是Ronronner,刚满18岁
string name = "Ronronner";
int age = 18;
Console.WriteLine($"我是{name},刚满{age}岁");
// 等价于Console.WriteLine("我是" + name + ",刚满" + age + "岁");
// 除此之外你还可以嵌表达式
// 输出 -> 结果是:5 + 3 = 8
int a = 5, b = 3;
Console.WriteLine($"结果是:{a} + {b} = {a + b}");
2. 接口(Interface)
接口这个东西,可以看作是一份合同
接口定义了合同内容(定义函数),即:合同 "是什么" ,
派生类实现了合同 "怎么做" 的部分(接口中的函数实现)
如果要和C++中的语法对比的话,第一时间会想到虚函数和纯虚函数,特别是纯虚函数
好巧不巧,C#中接口的原理也是依赖虚函数,虚函数表和虚函数指针的
项目
C++
C#
虚表结构
每个类有一张 vtable
每个类型有一张方法表(MethodTable)
对象内部
隐含一个 vptr 指针
隐含一个类型指针(TypeHandle)
多态调度
编译期确定表结构
CLR 运行时构建调度表
接口机制
用多继承或者纯虚函数实现(复杂)
用接口表(interface map)实现(安全)
可直接查看虚表
✅ 可反汇编看到地址
❌ 不可直接操作
内存模型
程序员可控
完全由 CLR 管理
// C++
class IFoo
{
public:
// 纯虚函数,表示这个类这个函数不需要实现,但是必须由基类实现
virtual void Test() = 0;
};
class Sub : public IFoo
{
public:
Test() { }
}
// ————————————————————————————————————————————————————————————————————————————————
// C#
// 接口
interface IFoo { void Test(); }
class Sub : IFoo
{
public void Test() { }
}
❗ ❗ 隐式接口 VS 显示接口
2025-11-21[新增]
// 找到一个特别有意思的示例代码,嘿嘿嘿
interface IPet
{
void Speak();
}
public class Cat : IPet
{
// 隐式接口 -> 猫娘对外
void IPet.Speak()
{
Console.WriteLine("(哈气) 嗤~~");
}
// 显示接口 -> 猫娘对内
public void Speak()
{
Console.WriteLine("喵~");
}
}
// ============================================
Cat cat = new Cat();
cat.Speak(); // 对内撒娇
((IPet)cat).Speak(); // 对外哈气
// 显示接口相当于
// IPet ipet = (IPet)cat;
// cat.Speak();
隐式接口实现
写成普通 public 方法,类实例可直接调用
简单、常用
显式接口实现
必须写成 void IA.Do(),只能通过接口引用调用
常用于方法名冲突、隐藏实现、保持 API 干净
💥C# 接口实现对比表
隐式接口实现
显式接口实现
写法
public void Do()
void IFoo.Do()
是否必须写接口名前缀
否
是
方法访问修饰符
必须 public
不能写 public(自动隐藏)
是否能被类实例直接调用
✔ 能
❎不能
是否能通过接口引用调用
✔ 能
✔ 能
目的
常规实现、公开 API
隐藏接口方法、解决方法名冲突
调用方式示例
a.Cat();
((IFoo)a).Cat();
避免成员名冲突
✘ 不支持
✔ 支持
是否影响 IntelliSense 显示
会显示
不会显示
使用场景
接口方法就是类功能
1.不想暴露方法
2.多个接口有同名方法(解决命名冲突)
3. 索引器(Indexer)
什么是索引器? 索引器允许一个对象可以像数组一样使用下标的方式来访问
但它实际上就相当于C++中写了一个 "[]" 运算符重载
/* ——————————————————————————————————————————————————————————————— */
// C++
#include <iostream>
using namespace std;
class MyList {
int data[5];
public:
int& operator[](int index) { return data[index]; }
};
int main() {
MyList list;
list[0] = 42;
cout << list[0] << endl; // 输出 42
}
/* ——————————————————————————————————————————————————————————————— */
// C#
using System;
class MyList
{
private int[] data = new int[5];
// 定义索引器
public int this[int index]
{
get => data[index];
set => data[index] = value;
// 相当于以下代码
// get
// {
// return data[index];
// }
// set
// {
// data[index] = value;
// }
}
}
class Program
{
static void Main()
{
MyList list = new MyList();
list[0] = 42;
Console.WriteLine(list[0]); // 输出 42
}
}
4.Object类,装箱与拆箱
(1)Object类
C# 的所有类型都派生自 System.Object
对象(Object)类型 是 C# 通用类型系统中所有数据类型的终极基类
Object 是 System.Object 类的别名
所以对象(Object)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值
但是,在分配值之前,需要先进行类型转换
Object类自带的4个方法:
方法名
作用说明
默认行为(若未重写)
常见用途
Equals(object obj)
判断两个对象是否相等
比较引用是否相同(即是否是同一个对象)
自定义比较逻辑时重写,例如比较值类型内容
GetHashCode()
返回对象的哈希值
基于引用生成一个哈希整数
用于哈希表、字典(Dictionary、HashSet)等
ToString()
返回对象的字符串表示形式
返回对象的类型名
打印日志、调试输出、自定义显示格式
GetType()
获取当前实例的运行时类型信息
返回对象的 System.Type 实例
反射(Reflection)中常用,用来查看类型结构
当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱
(2)装箱 & 拆箱
// 装箱: 将物品(值类型)丢进箱子(Object类型)封装起来 -> 将值类型转换为Object类型的过程
int x = 111;
object y = i;
// 拆箱: 将物品(值类型)从箱子(Object类型)里面拿出来 -> 从Object类型中提取值类型的过程
int z = (int)y;
// 因为C#中一切的祖宗都是Object, 所以可以在一个包装箱(object数组)中装不同的数据类型
Cat cat = new Cat()
object[] objs = {1, 2.5, "abcd", cat};
# 装箱与拆箱的内存原理
源数据存储在栈内存中
装箱数据 将源数据拷贝之后,存放于堆内存
拆箱数据 将堆内存数据拷贝之后,放于栈内存
┌───────────────────────────────────────────────┐
│ 内存布局示意图 │
├───────────────────────────────────────────────┤
│ │
│ Step1 源数据:值类型存在栈内存 │
│ │
│ ┌───────────────┐ │
│ │ Stack 栈内存 │ │
│ │ ───────────── │ │
│ │ int x = 42 │ ← 值类型存放在栈上 │
│ └───────────────┘ │
│ │
│ │
│ Step2 装箱:复制值数据到堆内存 │
│ │
│ ┌───────────────┐ ┌──────────────────────┐
│ │ Stack 栈内存 │ │ Heap 堆内存 │
│ │ ───────────── │ │ ──────────────────── │
│ │ int x = 42 │ │ [Object Header] │
│ │ object obj ─────────▶ │ [Value: 42] │
│ └───────────────┘ └──────────────────────┘
│ ↑
│ obj 是一个引用,指向堆中“盒子”
│ │
│ │
│ Step3 拆箱:复制堆中数据回到栈内存 │
│ │
│ ┌───────────────┐ ┌──────────────────────┐
│ │ Stack 栈内存 │ │ Heap 堆内存 │
│ │ ───────────── │ │ ──────────────────── │
│ │ int x = 42 │ │ [Object Header] │
│ │ object obj ───▶│ [Value: 42] │
│ │ int y = (int)obj ◀────────────────────────────┘
│ └───────────────┘
│ │
└───────────────────────────────────────────────┘
语法枯燥四问
什么是装箱和拆箱?
装箱(Boxing):把一个 值类型(如 int、double、struct) 转成 引用类型 object 的过程
拆箱(Unboxing): 把一个 object 再转回 原来的值类型 的过程
为什么需要他们?
C# 是“类型安全”的语言,但是有时需要把不同类型的数据混合在一起处理
但是值类型不能直接放进引用类型容器里,所以需要“装箱”成一个对象
装箱和拆箱的意义?
C# 的所有类型都派生自 System.Object,通过装箱,值类型也能被当作对象使用
即:统一类型体系,可以对数据统一处理
缺点?
装箱/拆箱都是 复制数据+分配堆内存 的操作,频繁发生会:
产生性能损耗(堆分配 + GC 压力)
拆箱类型不匹配会抛 InvalidCastException
好用吗?用性能换的
部分内容参考:88 C#教程 — 一切的祖宗object类——bilibili
5. 属性
属性,说白了就是包装字段(Field)的一种机制
再说直白点,就是 字段 + get/set 方法 的语法糖
再再说直白一点,就是 字段 +get/set 方法的简写版本
using System;
using static System.Console;
namespace PropertyDemo
{
public class Student
{
private string name = "嘤嘤嘤";
// 属性 - Property
// 以下是属性的常规写法
// 但是如果有业务需求,还可以在get和set中加上对数据的其他操作 - 例如,数据范围设置,数值转换等
public string Name
{
set { name = value; }
get { return name; }
}
// 常规Set和Get方法的写法 - C++/C
// public void SetName(string name)
// {
// this.name = name;
// }
// public string GetName()
// {
// return name;
// }
internal class Program
{
public static void Main()
{
Student s = new();
WriteLine(s.Name);
s.Name = "111";
WriteLine(s.Name);
}
}
}
参考:70 C#教程-字段与属性_哔哩哔哩_bilibili
6. 反射 & 特性
前言:反射和特性,我感觉是C#中最绕的两个概念了
一旦我们发现某个语法特别绕的时候,要不是翻译的问题,
要不就是很多教程他们自己都没想到怎么使用最简短的语言来描述他们
这个时候就可以去查单词原意或者翻英文文献了
(1)特性
特性,英文:Attribute
翻译:属性,特质;标志,象征
特性与其说叫特性,我们更应该称它为标签
[!IMPORTANT]
Attribute就是在程序集中加入更多的元数据(描述信息)
特性 = 带数据的标签
给类、方法等打上标签,编译器或反射代码就能读到这些标签携带的信息
至于为什么说,你使用特性之后可以修改一些东西
那不是你修改的,那是编译器看到的,然后看到这个标签了做出的回应
好比你去买衣服,你(编译器)看到了样品旁边写了 已售空(标签) ,你还会去问老板有没有吗
在菜鸟教程中,可以看到对C#特性的定义是这样的:
特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签,
可以通过使用特性向程序添加声明性信息,一个声明性标签是通过放置在它所应用的元素前面的方括号[ ]来描述的
特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息
.Net 框架提供了两种类型的特性:预定义特性和自定义特性
为什么需要特性?
某些情况下需要给类或者方法添加一些标签信息,比如我们在调试的时候为方法注明调试事件,调试人等等信息
根据定义方式的不同,特性分为系统提供的特性以及自定义的特性
// 1.预定义特性 -> 3种 -> AttributeUsage, Conditional, Obsolete
// (1) AttributeUsage -> 用于自定义特性
[AttributeUsage(
validon,
AllowMultiple = allowmultiple,
Inherited = inherited
)]
// (2) Conditional -> 编译时,按照给出的条件决定方法是否执行,用于 debug 时打印调试信息,release 时取消打印
[Conditional( conditionalSymbol )]
// (3) Obsolete -> 当程序修改时,某个方法弃用了,使用 Obsolete 做为标识
[Obsolete( message )] // 只提示警告信息
[Obsolete( message, iserror )] // iserror:bool类型,表示是否打印提示信息message
下面给出代码的详细例子:
1)Conditional
#define DE
// 踩坑点:为什么不能使用DEBUG
// 因为 DEBUG 这个符号是 Visual Studio / dotnet 项目默认定义的
// 所以,即使你注释掉 #define DEBUG,它依然存在
// 所以,不管你注释掉还是不注释掉,它还是会调用Logger.Log("程序开始运行");
// 破bug,我找了半天(哦,对了,示例代码我忘记加命名空间了,如果编译报错记得补上)
using System;
using System.Diagnostics;
// 编译原理
// [Conditional("Hello")] 是一种编译器特性
// 它告诉编译器:
// 1.如果当前项目中定义了符号 Hello,那么保留对这个方法的调用
// 2.否则,在编译阶段直接移除该调用
public class Logger
{
[Conditional("DE")]
public static void Log(string msg)
{
Console.WriteLine($"[DE] {msg}");
}
}
class Program
{
static void Main()
{
// 注释掉 #define DE 后 Log()不再启用
Logger.Log("程序开始运行");
Console.WriteLine("程序正在执行...");
}
}
2)Obsolete
using System;
public class MyClass
{
[Obsolete("请使用 NewMethod() 代替此方法")]
public static void OldMethod()
{
Console.WriteLine("这是旧方法");
}
public static void NewMethod()
{
Console.WriteLine("这是新方法");
}
}
class Program
{
static void Main()
{
MyClass.OldMethod(); // ⚠️ 编译器警告
MyClass.NewMethod(); // ✅ 正常
}
}
// 编译器警告但程序继续运行:
// 警告 CS0618: 'MyClass.OldMethod()' 已过时: '请使用 NewMethod() 代替此方法'
// 终端打印结果:
// 这是旧方法
// 这是新方法
using System;
public class MyClass
{
[Obsolete("OldMethod 已弃用,请使用 NewMethod()", true)]
public static void OldMethod()
{
Console.WriteLine("这是旧方法");
}
public static void NewMethod()
{
Console.WriteLine("这是新方法");
}
}
class Program
{
static void Main()
{
MyClass.OldMethod(); // ❌ 编译错误,无法通过
}
}
// 编译器报错 且程序中止:
// 错误 CS0619: 'MyClass.OldMethod()' 已过时: 'OldMethod 已弃用,请使用 NewMethod()'
3)AttributeUsage
参数
含义
AttributeTargets.Class
允许贴在类上
AttributeTargets.Method
允许贴在方法上
AttributeTargets.Property
允许贴在属性上
AllowMultiple = true
允许在同一个目标上贴多个 [DebugInfo] 标签
using System;
using System.Diagnostics;
using System.Reflection;
// 1.定义特性类
public class DebugInfo : Attribute
{
public int? BugNo { get; } // Bug编号
public string? Developer { get; } // 开发者
public string? LastReview { get; } // 最后复查日期
public string? Message { get; set; } // 说明
// 构造函数:初始化必填参数
public DebugInfo(int bugNo, string developer, string lastReview)
{
BugNo = bugNo;
Developer = developer;
LastReview = lastReview;
}
}
// 2.使用自定义特性(标签)
[DebugInfo(45, "Ronronner", "2025-11-11", Message = "优化矩形计算逻辑")]
class Rectangle
{
[DebugInfo(55, "Zara Ali", "2012-10-19", Message = "验证边长输入")]
public double GetArea(double width, double height)
{
return width * height;
}
}
// 3.通过反射读取特性
// 所谓的反射则是把类或方法的标签信息提取出来
class Program
{
static void Main()
{
Type type = typeof(Rectangle);
// 读取类上的特性
foreach (DebugInfo attr in type.GetCustomAttributes(typeof(DebugInfo), false))
{
Console.WriteLine($"类Bug编号:{attr.BugNo}, 开发者:{attr.Developer}, 时间:{attr.LastReview}, 备注:{attr.Message}");
}
// 读取方法上的特性
foreach (MethodInfo method in type.GetMethods())
{
foreach (DebugInfo attr in method.GetCustomAttributes(typeof(DebugInfo), false))
{
Console.WriteLine($"方法:{method.Name}, Bug编号:{attr.BugNo}, 开发者:{attr.Developer}, 时间:{attr.LastReview}, 备注:{attr.Message}");
}
}
}
}
// 类Bug编号:45, 开发者:Ronronner, 时间:2025-11-11, 备注:优化矩形计算逻辑
// 方法:GetArea, Bug编号:55, 开发者:Zara Ali, 时间:2012-10-19, 备注:验证边长输入
(2)反射
反射,英文:Reflection
翻译:反射;(反射出来的)影像
使用特性,必须用到反射
但是使用反射,不一定会用到特性
[!IMPORTANT]
反射:运行时获取程序集中的元信息
首次接触这个概念会非常的一脸懵逼
但是只需要记住一件事情就可以了
反射 的作用就是 程序在运行时 可以查看或者修改程序的内容的(包括私有成员和静态成员)
学术表达:反射可以动态地访问(查看)、修改和调用那些编译时无法直接触碰的私有成员或静态成员
简单粗暴的说就是,程序在运行时你可以把它底裤都扒光,还能顺手在底裤上面改一改花纹
终于找到比友元还危险的家伙了.JPG
反射代码示例
using System;
using static System.Console;
using System.Reflection; // 反射命名空间
namespace AttributeAppl
{
class MyClass
{
private string myField = "LOG : 私有字段";
private static string sField = "LOG : 静态私有字段";
private string myProperty { get; set; } = "LOG : 私有属性";
private void FunA() { WriteLine("LOG : 执行私有方法"); }
private static void FunB() { WriteLine("LOG : 执行静态私有方法"); }
}
class Program
{
static void Main(string[] args)
{
MyClass myClass = new();
/* 1.获取类型对象 Type */
Type type = myClass.GetType();
/* 2.定义搜索范围 - */
/* Instance -> 访问实例成员 */
/* NonPublic -> 访问非公共成员 */
/* Static -> 访问静态成员 */
BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;
/* 3.访问 字段 - FieldInfo */
FieldInfo? fieldInfo = type.GetField("myField", flags);
WriteLine(fieldInfo?.GetValue(myClass));
fieldInfo?.SetValue(myClass, "迟早要疯");
WriteLine(fieldInfo?.GetValue(myClass));
/* 访问 静态字段 */
fieldInfo = type.GetField("sField", flags);
WriteLine(fieldInfo?.GetValue(myClass));
fieldInfo?.SetValue(myClass, "已经疯了");
WriteLine(fieldInfo?.GetValue(null));
WriteLine("=============================================================");
/* 3.访问 属性 - PropertyInfo */
PropertyInfo? propertyInfo = type.GetProperty("myProperty", flags);
WriteLine(propertyInfo?.GetValue(myClass));
propertyInfo?.SetValue(myClass, "我不是私有属性");
WriteLine(propertyInfo?.GetValue(myClass));
WriteLine("=============================================================");
/* 3.访问 方法 - MethodInfo */
MethodInfo? method = type.GetMethod("FunA", flags);
method?.Invoke(myClass, null); // 实例方法用对象调用
method?.Invoke(null, null); // ❌ 能编译和执行代码,但是执行该处时会抛异常
// 因为 非静态方法调用必须要有对象
method = type.GetMethod("FunB", flags);
method?.Invoke(myClass, null); // myclass被忽略
method?.Invoke(null, null); // 静态方法调用传 null 即可(因为有对象也会被忽略,活该单身)
// 因为 静态方法调用不需要对象
}
}
}
当然,对于初学者来说,看到这里,只能懂反射的作用
如果你还不太明白反射是这样使用的,没关系,我们逐步拆解
什么,你说你懂了?如果是天赋狗,给我死
/* 1.获取类型对象 Type */
Type type = myClass.GetType();
/* 2.定义搜索范围 */
BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;
/* 3.创建对应的反射类 */
FieldInfo? fieldInfo = type.GetField("myField", flags);
/* 4.打印需要访问的字段 */
WriteLine(fieldInfo?.GetValue(myClass));
/* 5.修改对应的字段 */
fieldInfo?.SetValue(myClass, "迟早要疯");
WriteLine(fieldInfo?.GetValue(myClass));
# 现在从语法层面逐行拆解一下上面的代码
# 1.Type type = myClass.GetType();
# Type -> 运行时类型描述对象
type 描述的是 myClass 本身的实际类型(类名、方法、属性、程序集等元信息)
System.Type 是 .NET 用来表示"类型(class/struct/interface/enum/array/delegate 等)"的抽象对象
同时它也是反射的核心,提供了访问类型元数据的能力
# 2.BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;
BindingFlags 是一个 枚举类型(Enum),定义在 System.Reflection 命名空间中
flags实际上是一个搜索条件集合,也就是告诉编译器你想查找哪些成员
这里的几个条件
Instance -> 访问实例成员
NonPublic -> 访问非公共成员
Static -> 访问静态成员
# 3.FieldInfo? fieldInfo = type.GetField("myField", flags);
(1)FieldInfo -> C# 中System.Reflection 命名空间提供的反射类,表示类中的一个字段(Field)它属于
通过它你可以获取、修改字段的值,查看类型、修饰符等信息
同理:
访问 字段 - FieldInfo
访问 属性 - PropertyInfo
访问 方法 - MethodInfo
(2)FieldInfo? fieldInfo -> 这里回顾一下可空引用类型修饰符 "?"
"FieldInfo?" 表示 fieldInfo 变量可能为 null, 加上 "?" 是为了避免编译器警告
比如当找不到名为 "myField" 的字段时,GetField() 会返回 null
(3)type.GetField("myField", flags);
type 是一个 Type 对象,它代表 MyClass 这个类型的蓝图
GetField(string name, BindingFlags flags) 是反射中用来根据字段名搜索字段信息的方法
# 4.WriteLine(fieldInfo?.GetValue(myClass));
if (fieldInfo != null)
WriteLine(fieldInfo.GetValue(myClass));
# 语法就不复习了,都看到这了不信还不知道是什么意思
# GetValue(object obj) -> 获取该字段在实例对象 obj 上的值
fieldInfo?.GetValue(myClass) -> 从对象 myClass 里读取 myField 的当前值
#5.fieldInfo?.SetValue(myClass, "迟早要疯");
fieldInfo?.SetValue(myClass) -> 从对象 myClass 里修改 myField 的当前值
常用 BindingFlags 枚举值及其含义
枚举值
含义
说明
Public
公共成员
只搜索 public 的字段、方法、属性等
NonPublic
非公共成员
搜索 private、protected、internal 等
Instance
实例成员
搜索属于对象实例的成员
Static
静态成员
搜索属于类型本身(类)的成员
DeclaredOnly
仅搜索当前类声明的成员
不包含继承自父类的成员
FlattenHierarchy
搜索静态成员时包含继承的
通常用于搜索基类静态字段或属性
IgnoreCase
忽略名称大小写
GetField("MyField", flags) 时不区分大小写
IgnoreReturn
忽略返回值匹配
几乎不用,反射方法匹配时可忽略返回类型
Default
默认值
等价于没有指定任何搜索选项
CreateInstance
用于 Activator.CreateInstance 创建实例时
指示反射如何绑定构造函数
GetField / SetField
指定绑定字段时的行为
常配合 InvokeMember 使用
GetProperty / SetProperty
指定绑定属性时的行为
同上,用于 InvokeMember
资料参考目录:
1.C# 特性(Attribute) | 菜鸟教程 及其评论区
2.C# 反射与特性系列教程 · C# 反射教程大全 - 痴者工良
3.C#基础教程 Attribute 特性与反射案例详解,自动化识别与使用类型!_哔哩哔哩_bilibili
4.C#中的反射与特性大致是怎么一回事_哔哩哔哩_bilibili
5.C#基础教程 Reflection应用,简单使用反射,打破常规!_哔哩哔哩_bilibili
7.泛型与泛型约束
(1)泛型
C#的泛型和C++模板区别感觉不是特别大
C++函数模板 vs C#泛型方法
// C++函数模板
template <typename T>
T Add(T a, T b) {
return a + b;
}
int main() {
cout << Add(1, 2) << endl; // 编译器生成 Add<int>
cout << Add(1.5, 2.5) << endl; // 编译器生成 Add<double>
}
/* ==================================================================== */
// C#函数泛型
public static T Add<T>(T a, T b) where T : struct
{
dynamic x = a, y = b;
return (T)(x + y);
}
Console.WriteLine(Add(1, 2));
Console.WriteLine(Add(1.5, 2.5));
对比点
C++ 模板
C# 泛型
生成机制
编译期生成独立版本(代码复制)
运行期类型实例化(共享IL)
检查时机
编译期
编译期检查 + 运行时绑定
性能
原生展开(最快)
稍慢(JIT需处理)
灵活性
可模板元编程
不支持元编程
本质
静态多态
运行时类型安全泛化
特别说明
C#泛型中,禁止在泛型函数或者泛型类中对相关变量定义为dynamic类型
例如dynamic d = 20;,是因为泛型是在编译期进行检查,而dynamic 关键字是在运行时检查
public T Add<T>(T a, T b)
{
return (dynamic)a + (dynamic)b; // ⚠️
}
Console.WriteLine(Add(1, 2)); // ✅ OK
Console.WriteLine(Add("A", "B")); // ✅ OK
Console.WriteLine(Add(true, false)); // ❌ 运行时异常:Operator '+' cannot be applied
C++类模板 vs C#泛型类
// C++类模板
#include <iostream>
using namespace std;
template <typename T>
class Box
{
public:
T value;
Box(T v) : value(v) {}
void Show() { cout << "Value: " << value << endl; }
};
int main()
{
Box<int> b1(10);
Box<string> b2("Hello");
b1.Show(); // 10
b2.Show(); // Hello
}
/* ========================================================= */
// C#泛型类
using System;
class Box<T>
{
public T Value { get; set; }
public Box(T v) { Value = v; }
public void Show() => Console.WriteLine($"Value: {Value}");
}
class Program
{
static void Main()
{
Box<int> b1 = new Box<int>(10);
Box<string> b2 = new Box<string>("Hello");
b1.Show(); // 10
b2.Show(); // Hello
}
}
/* ========================================================= */
// C#泛型接口
using System;
interface IProcessor<T>
{
void Process(T value);
}
class IntProcessor : IProcessor<int>
{
public void Process(int value)
{
Console.WriteLine($"Processing int: {value}");
}
}
class Program
{
static void Main()
{
IProcessor<int> p = new IntProcessor();
p.Process(42);
}
}
C++函数包装器std::function+委托 VS C#模板委托
// C++函数包装器std::function + 委托
#include <iostream>
#include <functional>
using namespace std;
template<typename T>
class Notifier {
public:
function<void(T)> callback;
void SetCallback(function<void(T)> cb) {
callback = cb;
}
void Notify(T value) {
if (callback) callback(value);
}
};
int main() {
Notifier<int> notifier;
notifier.SetCallback([](int x) {
cout << "Received: " << x << endl;
});
notifier.Notify(100);
}
/* ========================================================= */
// C#泛型委托
using System;
delegate void NotifyHandler<T>(T value);
class Notifier<T>
{
public NotifyHandler<T> OnNotify;
public void Notify(T value)
{
OnNotify?.Invoke(value);
}
}
class Program
{
static void Main()
{
var notifier = new Notifier<int>();
notifier.OnNotify += (x) => Console.WriteLine($"Received: {x}");
notifier.Notify(100);
}
}
参考:嵌入式面试题 - C++总结(三) - 假设狐狸有信箱 - 博客园
(2)泛型约束(where 关键字)
什么是泛型约束:用来限制泛型参数类型范围的
泛型约束(where) 是告诉编译器:这个类型参数 T 必须满足某些条件,才能被用在这里
即:对泛型中传入的类型进行检查,规定必须满足对应的条件
// 泛型类
class ClassName<T> where T : 约束类型
{
// 泛型方法
void Func<T>(T obj) where T : 约束类型
{
// Todo...
}
}
🧩 C# 常见 6 种泛型约束
约束写法
含义
示例
where T : struct
T 必须是 值类型
List<int> ✅;List<string> ❌
where T : class
T 必须是 引用类型
MyClass<string> ✅;MyClass<int> ❌
where T : new()
T 必须有无参构造函数
如果与其他约束共用,必须放在最后
例如:where T : class, new()
用于 new T()
where T : Class
T 必须继承指定基类
如果与其他约束共用,必须放在最前面
例如:where T : class, new()**
where T : Enemy
where T : IInterface
T 必须实现某个接口
where T : IDisposable
where T : class, new()
多个约束组合
引用类型 + 可实例化
# 泛型限定条件:
# where T:结构 -> 类型参数必须是值类型,可以指定除 Nullable 以外的任何值类型
# where T:类 -> 类型参数必须是引用类型,包括任何类、接口、委托或数组类型
# where T:new() -> 类型参数必须具有无参数的公共构造函数
# 当与其他约束一起使用时new() 约束必须最后指定
# where T:<基类名> -> 类型参数必须是指定的基类或派生自指定的基类
翻译:假设 T:<基类名> ,这个类名称叫A,
表示 T 必须实现A(即 T 要么本身就是 A),或者实现的类继承过这个接口
# where T:<接口名称> -> 类型参数必须是指定的接口或实现指定的接口
翻译:假设 T:<接口名称> ,这个接口名称叫A,
表示 T 必须实现A(即 T 要么本身就是 A),或者实现的接口继承过这个接口
# 可以指定多个接口约束,约束接口也可以是泛型的
# 多泛型约束:
# public class AClass<T, B> where T : IOne where B : class
[!WARNING]
class ClassName<T> where T : person 这个T必须是Person本类或者其子类,孙子类
class Person {}
class Student : Person {}
class Pupil : Student {}
class Dog {}
class ClassName<T> where T : Person {}
class Test
{
void Run()
{
ClassName<Person> a = new ClassName<Person>(); // ✅ OK
ClassName<Student> b = new ClassName<Student>(); // ✅ OK
ClassName<Pupil> c = new ClassName<Pupil>(); // ✅ OK
ClassName<Dog> d = new ClassName<Dog>(); // ❌ 错误:Dog 不是 Person 的子类
}
}
参考:C# 泛型(Generic) | 菜鸟教程
8. 委托
(1)委托定义
委托的本质是一种类型安全的函数指针,从功能上来看就是天王老子来了他也是C++中的函数包装器std::function
不知道是哪个大天才将委托搞的这么复杂的,找了一堆视频和文章,哇哇哇哇讲一大堆,还讲不明白,以为是多么高深的语法一样
委托,委托,说白了也就是 声明,赋值,调用,再说白了就是将一个函数包装起来再调用
在此之前先说一下类和委托
类Class -> 实例化 -> 对象
委托 -> 实例化 -> 委托实例
# 普通变量存放的是数据
# 委托 存放的行 为(类或对象能够执行的动作,包括方法、事件、委托、操作符重载等)
# C#的行为和C++的行为需要区分开来(但是基本上都是指类或对象能够执行的动作)
# C#中行为:包括方法、事件、委托、操作符重载等
# C++行为:通常指 成员函数,全局函数不属于类,故不是行为
(2)委托的使用
// 委托是如何使用的:声明 -> 实例化 -> 赋值 -> 调用
using System;
using static System.Console;
namespace Program
{
// 1.声明
// 你可以在命名空间中定义一个类,接口或者委托
// 但是不能定义一个变量和函数
// 定义一个委托
delegate void Help();
// 定义一个类
public class Person { }
public class Program
{
public static void Main()
{
// 2.实例
Help h;
Person p;
// 3.赋值
// 将一个函数赋值给一个委托实例
h = SayHello;
// 4.调用
h();
h();
void SayHello()
{
WriteLine("哇哇哇哇");
}
}
}
}
(3)委托的作用
委托是 也就是 行为的参数化 ,让函数能像数据一样使用(传递、存储、组合和执行)
/* 极简版 */
using System;
using static System.Console;
class Program
{
static void Main(string[] args)
{
void GoStation(Action do_sth)
{
WriteLine("去火车站");
WriteLine("找到站长");
do_sth();
WriteLine("离开火车站");
}
GoStation(() => WriteLine("打他一顿"));
}
}
// ===============================================================
/* 详解一点点版 */
using System;
using static System.Console;
namespace Demo
{
// 定义一个委托类型
delegate void Help();
public class Program
{
static void Main()
{
// 定义几个行为
// 相当于void SayHello() => WriteLine("跟站长打招呼");
void SayHello()
{
// 你不仅可以Hello, 还可以给站长一拳, 只不过还是给两拳好, 左拳伤害高, 右拳高伤害, 平A接普攻, 伤害高又高
// WriteLine("Hello");
WriteLine("给站长一拳");
}
void BuyTicket() => WriteLine("买火车票");
void TakePhoto() => WriteLine("发布照片");
// 定义流程
void GoStation(Help do_sth)
{
WriteLine("去火车站");
WriteLine("找到站长");
do_sth(); // 执行所有传入的行为
WriteLine("离开火车站");
}
// 委托 - 单一行为
GoStation(SayHello);
// 委托 - 多播行为
Help actions = SayHello;
actions += BuyTicket;
actions += TakePhoto;
GoStation(actions);
}
}
}
(4)3种委托代码示例:Action,Funtion,delegate
委托类型
用途
示例
Action
执行操作,无返回值
Action report = cal.Report;
Func<T1, T2, TResult>
T1,T2... : 参数1,参数2...
Tesult:函数返回值
执行操作,有返回值
Func<int,int,int> add = cal.Add;
delegate
自定义委托类型
delegate int Demo(int a, int b);
Demo demo = cal.Add;
using System;
namespace ActionDemo
{
public class Calculator
{
public void Report() => Console.WriteLine("Running...");
public int Add(int a, int b) => a + b;
public int Sub(int a, int b) => a - b;
}
public class Program
{
static void Main()
{
// 类实例化
Calculator cal = new();
// 1.Action:无返回值委托
// Action action = new Action(cal.Report);
// cal.Report();
Action action = cal.Report;
action();
// 2.泛型委托Func:带返回值的委托(最后一个类型是返回值类型)
// Func<int, int, int> addFunc = new Func<int, int, int>(cal.Add);
Func<int, int, int> addFunc = cal.Add;
Func<int, int, int> subFunc = cal.Sub;
int x = 100, y = 200;
// z = subFunc(x, y); // z = addFunc.Invoke(x, y);
// Console.WriteLine(z);
Console.WriteLine($"Add: {addFunc(x, y)}");
Console.WriteLine($"Sub: {subFunc(x, y)}");
}
}
}
参考我找到的关于委托最好的教学视频:之所以你没有使用委托是因为你还不够了解它.._哔哩哔哩_bilibili
9. 事件
[!IMPORTANT]
使用事件必然会用到委托,因为事件本质上就是对委托的一种封装和限制
什么事件声明,什么订阅,什么访问器,还没有学习的时候以为多么多么高深,然后学完了
我感觉,事件总的来说,他不适合成为一种语法,而更适合成为一种思维模式
什么事件声明,订阅,事件访问器,统统不用看,看了有用吗,没用啊,
光记那种东西有什么用呢,而且谁记得住啊,谁没事记那种东西啊
什么是事件,你定义了一个函数,这个函数只有在按钮A按下后才会触发
这个函数就是按键A的事件,哪有那么复杂
如果说C++中什么语法可以用来说明事件,QT中的信号和槽函数再合适不过了
信号中绑了一个函数,有一个行为来触发这个触发信号,从而进一步调用特定函数
下面使用QT中的信号和槽来讲解一下事件的基本思想
/* QT信号和槽函数 */
connect(ui->prevBtn, &QPushButton::clicked, this, &Widget::handlePrevSlot);
// connect 是一个用于 建立信号(signal)与槽(slot)之间连接关系 的函数
// 也就是把"事件源"的动作,连接到"响应者"的函数上
// 当 按键prevBtn 触发 clicked() 点击信号时
// Qt 的 元对象系统 (Meta-Object System) 会通过 事件队列或直接调用 的方式
// 让 handlePrevSlot() 被执行
// 用大白话讲,就是,你按下了按钮prevBtn,会触发按钮prevBtn按钮被按下的信号,然后调用函数handlePrevSlot()
那么,C#中是怎么实现事件的呢?
让我们短话长说
// 为什么使用事件,用下面一个特别特别特别特别特别长的案例说明一下
// 下面是事件的畸形
using System;
using static System.Console;
namespace SeniorEvent
{
// 委托方法导致携带参数过多,当参数需求变动时,难以维护
// 所以需要事件将所有参数包含到一个类中
// 事件参数们
public class EventArgs
{
public int attack;
public bool poisned;
public bool headache;
}
// 怪物Enemy
public class Enemy
{
private int blood = 100; // 血量
public void MinusBlood(object o, EventArgs args)
{
WriteLine("Enemy受到了伤害");
blood -= args.attack;
if(args.poisned)
{
WriteLine("玩家中毒了");
}
if(args.headache)
{
WriteLine("Enemy眩晕了");
}
// 装箱
Player player = (Player)o;
player.Shout();
}
}
// NPC类
public class NPC
{
private int blood = 100; // 血量
public void BeAttackTest(object x, EventArgs args)
{
WriteLine("NPC受到了伤害");
blood -= args.attack;
}
}
// 使用委托解耦合
// 在Player当中声明委托类型, 将需要调用的减血方法在Player类外设置给内部的委托
// 玩家 -> AOE
public class Player
{
// 声明并且初始化一个委托
public delegate void OnattackDelegate(object x, EventArgs args);
public OnattackDelegate? Onattack = null;
public void DoAOE()
{
EventArgs args = new EventArgs();
args.attack = 10;
args.poisned = true;
Onattack?.Invoke(this, args);
}
internal void Shout()
{
WriteLine("反甲很痛");
}
}
internal class Program
{
static void Main(string[] args)
{
Player player = new Player();
Enemy enemy0 = new Enemy();
Enemy enemy1 = new Enemy();
Enemy enemy2 = new Enemy();
NPC npc = new NPC();
player.Onattack += enemy0.MinusBlood;
player.Onattack += enemy1.MinusBlood;
player.Onattack += enemy2.MinusBlood;
player.Onattack += npc.BeAttackTest;
player.DoAOE();
}
}
}
虽然上面的代码也是事件
但是在语法方面我们还可以更加简化
using System;
using static System.Console;
/*
* 事件(Event关键字)
* Event修饰的委托,只能
* 1.在类内被调用执行,类外不可被调用执行
* 2.类外不可被直接赋值,只能通过"+""-"增减方法
*/
namespace EventDemo
{
public class Player
{
// 定义Player会被触发的事件委托
// 原理:EventHandler是一个System内部已经定义的全局委托类型
// 实际上就是一句 -> public delegate void EventHandler(object? sender, EventArgs e);
public event EventHandler? OnAttack;
// 旧写法 -> C# 8.0 引入可空引用类型导致编译器警告,但是安全可用
// public event EventHandler OnAttack = null;
public void DoAOE()
{
if (OnAttack != null)
{
OnAttack(this, EventArgs.Empty);
}
WriteLine("玩家进行了攻击!");
}
}
public class Emeny
{
public void AttackMe(object? sender, EventArgs e)
{
WriteLine("Emeny被攻击了!");
}
}
internal class Program
{
static public void Main()
{
Player player = new();
Emeny emeny = new();
player.OnAttack += emeny.AttackMe;
player.DoAOE();
// 禁止 在类外直接调用event修饰的委托去执行
// 2种错误写法
// 1.
// player.OnAttack(new object(), EventArgs.Empty);
// 2.
// EventHandler handler = new EventHandler(e.AttackMe); // 定义
// player.OnAttack = handler;
// handler(new object(), EventArgs.Empty);
}
}
}
资料参考:93. C#教程-事件Event概念_哔哩哔哩_bilibili
10.Lambda与匿名函数
匿名函数,顾名思义就是没有名字的函数,函数名被隐藏了
而 Lambda 又属于是 匿名函数 的一种
你可以使用匿名方法创建一个委托
至于C++的匿名函数,感觉可以讲大半天,就不过多叙述了
Lambda匿名函数语法:
(参数列表) =>
{
函数体
}
如果参数只有一个 -> 可以省略"()"
如果函数只有一句 -> 可以省略"{}"或者省略"return"
/* 示例 */
/* ==================================================== */
// 一.原始版本
// 1.定义一个普通命名函数
static int AddOne(int x)
{
return x + 1;
}
static void Main()
{
// 2.使用系统自带的 Func 委托类型
Func<int, int> f = new Func<int, int>(AddOne);
// 3.调用委托(其实就是调用 AddOne)
Console.WriteLine(f(5)); // 输出 6
}
/* 二.匿名函数版本 */
Func<int, int> f = delegate (int x)
{
return x + 1;
};
/* 三.Lambda表达式版本 */
Func<int,int> f = x => x + 1;
/* 1.匿名方法 -> 使用委托 */
// 可用于直接赋给 delegate/Func/Action
Func<int,int> f = delegate(int x) { return x + 1; };
Console.WriteLine(f(3)); // 4
/* 2.Lambda表达式 */
// (1)表达式
// 右侧是单一表达式,编译器自动返回该表达式值
// 可被编译为 委托 Func/Action 或 表达式树 Expression<Func<...>>
// LINQ 非常常见
Func<int,int> f = x => x + 1;
// Func<int,int> f = (x) => { x + 1; }
Func<int,bool> isEven = n => n % 2 == 0;
// (2)语句
Func<int,int,int> add = (a, b) => { var s = a + b; return s; };
Action hello = () => { Console.WriteLine("hi"); };
// Action hello = () => Console.WriteLine("hi");
/* 3.参数写法:类型显式或省略(类型推断) */
Func<int,int,int> sum1 = (int a, int b) => a + b; // 显式类型
Func<int,int,int> sum2 = (a, b) => a + b; // 推断类型(常用)
Func<int> get = () => 42; // 无参数时用 ()
🍀🍁关于 C# 中=> 语法糖的讲解
// 这一个语法糖不太好从概念上去理解,但是一旦讲应用,瞬间就可以理解
// 我们来看写属性时写的代码
public int ID
{
set { id = value; }
get { return id; }
}
// 为了偷懒我们可以这样写
public int ID
{
set => id = value; // 等价于 set { id = value; }
get => id; // 等价于 get { return id; }
}
// 同理,下面的一些代码可以偷懒了(第一句为正常代码,第二局为偷懒的代码)
// 建议C++引进一下,谢谢
public int Add(int a, int b) { return a + b; }
public int Add(int a, int b) => a + b;
public string Name { get { return "Ronronner"; } }
public string Name => "Ronronner"; // 这个属性里面一定只有get没有set
public Person(string n) { name = n; }
public Person(string n) => name = n;
~Person() { Console.WriteLine("Bye"); }
~Person() => Console.WriteLine("Bye");
public string this[int i] { get { return data[i]; } }
public string this[int i] => data[i];
11. 垃圾回收机制(GC机制)
C#中的GC机制原理是分代算法(核心算法)和标记压缩算法
你要是讲算法,那我可就犯困了啊,所以初学者了解一点点就可以了
#include <iostream>
using namespace std;
class Dog {
public:
Dog() { cout << "开门放狗🐕" << endl; }
~Dog() { cout << "闭门打狗🐕" << endl; }
};
int main() {
Dog* p = new Dog(); // 手动分配
cout << "Playing...\n";
delete p; // 必须手动释放
return 0;
}
using System;
using static System.Console;
class Dog {
public Dog() => WriteLine("开门放狗🐕");
~Dog() => WriteLine("闭门打狗🐕");
}
class Program {
static void CreateDog()
{
Dog dog = new Dog();
WriteLine("你创造了一只狗......");
// 离开作用域后 dog 被销毁引用,成为垃圾候选
}
static void Main()
{
/* 1.第一只狗 */
Dog dog = new Dog(); // 自动分配堆内存
WriteLine("Playing...");
dog = null;
GC.Collect(); // 触发GC(演示用)
// 只有在有对象进入析构函数时才会阻塞,没有任务时会直接返回
GC.WaitForPendingFinalizers();
/* 2.第二只狗 */
CreateDog();
GC.Collect();
WriteLine("-------------------");
GC.WaitForPendingFinalizers();
WriteLine("-------------------");
}
}
// 1.在你第一次手动触发GC机制时,第一只狗虽然被设为null,但它仍然处于 Main 方法的活动栈帧中
// 编译器出于性能考虑,通常会延长局部变量的"存活期"(Variable Lifetime),
// 导致 GC 在扫描时认为 dog 对象"仍然可使用",
// 因此它不会触发终结器,所以不会触发 ~Dog()
// 即:第一只狗没有被标记为待终结, 所以不会被触发GC机制
// 但是具体行为依赖于编译模式:(在实际开发中,我们通常不依赖终结器来管理资源)
// Debug模式:第一只狗可能不会被立即回收
// Release模式:第一只狗很可能被回收
// 2.这里只有第二只狗触发了析构函数,即资源完全被释放
总的来说,就是C++内存管理需要全方面依靠自己
而C#中GC机制它会自动调用
[!CAUTION]
🧩 C++内存管理和C#垃圾回收机制对比表
项目
C++
C#
分配方式
new
new
释放方式
delete 手动
GC 自动
内存泄漏风险
高
低
清理算法
无(手动)
分代算法 + 标记压缩算法
析构函数
立即执行
延迟执行(终结器)
控制权
开发者
GC 管理器
性能
精细可控
稍慢但安全
12. 集合
[!IMPORTANT]
集合 的 命名空间 -> System.Collections.Generic
C#中的集合感觉就是C++中容器的翻版
不对,应该把感觉去掉,就是模拟C++的容器的,
只是底层换成了 GC 管理内存 + 类型安全 + CLR JIT 优化
🧩C++ STL 容器 与 C# 集合 对比表
分类
C++ STL 容器
C# 对应集合类型
说明 / 区别点
动态数组
std::vector<T>
List<T>
连续存储,随机访问 O(1),自动扩容
C#中的List<T> 基本是托管版 vector
双端队列
std::deque<T>
无
deque 是双端动态数组
C# 没有完全等价结构,LinkedList<T> 更像 std::list
链表
std::list<T>
LinkedList<T>
双向链表,插入/删除快
C# 的 LinkedList<T> 是直接对应实现
栈
std::stack<T>
Stack<T>
后进先出 (LIFO)
C# 版本是泛型类封装,不用模板适配器
队列
std::queue<T>
Queue<T>
先进先出 (FIFO),两者功能基本一致
集合(不重复元素)
std::set<T>
HashSet<T>
基于哈希表实现,自动去重,查找快
有序集合
std::set<T> (红黑树)
SortedSet<T>
C# 版也基于红黑树,元素自动排序
键值对
std::map<K,V>
SortedDictionary<K,V>
都是有序映射(基于树),按键排序
无序映射
std::unordered_map<K,V>
Dictionary<K,V>
哈希表实现,查找效率高但无序。
数组封装
std::array<T,N>
T[]
这是内置数组
不是集合
固定大小数组,内存连续
C# 原生数组更接近它
对比点
C++ STL
C# 集合
泛型机制
模板 (编译期实例化)
泛型 (运行时 + JIT)
内存管理
手动(RAII)
自动(GC 管理)
性能控制
可自定义分配器 / 指针
托管内存,性能稍逊但更安全
线程安全
默认不安全
某些集合有线程安全版本(如 ConcurrentDictionary)
算法支持
<algorithm> 提供丰富算法
LINQ 提供强大查询功能
可读性/开发效率
偏底层
高层、简洁、直观
// 初始化的不同, 你问使用啊,随用随查,用多了就记住了
// 第一段 为 C++
// 第二段 为 C#
/* 1.动态数组 */
std::vector<int> arr = {1, 2, 3, 4};
List<int> arr = new List<int> {1, 2, 3, 4};
/* 2.链表 */
std::list<int> link = {10, 20, 30};
LinkedList<int> link = new LinkedList<int>(new int[] {10, 20, 30});
/* 3.队列 */
std::queue<int> q;
q.push(1);
Queue<int> q = new Queue<int>();
q.Enqueue(1);
/* 4.栈 */
std::stack<int> s;
s.push(10);
Stack<int> s = new Stack<int>();
s.Push(10);
/* 5.堆 */
std::priority_queue<int> heap;
heap.push(5);
PriorityQueue<int, int> heap = new PriorityQueue<int, int>();
heap.Enqueue(5, 5);
/* 6.键值对 */
std::map<std::string, int> dict = {
{"apple", 1},
{"banana", 2}
};
Dictionary<string, int> dict = new Dictionary<string, int>
{
{"apple", 1},
{"banana", 2}
};
13. 异常
C++和C#的异常,感觉C#多加了一个小尾巴finally
C++异常 -> try-catch 结构
C#异常 -> try-catch-finally 结构
/* C++异常处理 */
try
{
// 可能抛出异常的代码
}
catch (const ExceptionType1& e1)
{
// 处理类型1异常
}
catch (...)
{
// 兜底:处理未知类型异常
}
// ⚠️ C++ 没有 finally,需要类似功能要靠 RAII(例如析构函数) 或智能指针
/* ———————————————————————————————————————————————————————————————————————— */
/* C#异常处理 */
try
{
// 可能抛出异常的代码
}
catch (ExceptionType1 e1)
{
// 处理类型1异常
}
catch (Exception)
{
// 兜底:处理未知类型异常
}
finally
{
// 无论是否异常都会执行
}
项目
C++
C#
异常类型
可以抛任意类型(int、string、类…)
必须继承自 Exception
catch 匹配方式
匹配类型(值/引用/指针)
只匹配类层次结构
finally
❌ 没有 finally
✅ 有 finally
资源释放方式
依赖 RAII(构造/析构自动释放)
依赖 finally 或 using(IDisposable)
异常规范
很少用,甚至被弃用
严格依靠 Exception 体系
性能
抛异常很贵;一般不用异常做流程控制
也很贵,但常用于业务逻辑错误
throw = 主动抛异常
/*
* C++ throw代码示例
*/
#include <iostream>
#include <stdexcept>
void DoSomething()
{
// 主动抛异常(可以抛任何类型)
throw std::runtime_error("恭喜你,出错啦!");
}
void Test()
{
try
{
DoSomething();
}
catch (const std::runtime_error& e)
{
std::cout << "捕获到异常: " << e.what() << std::endl;
}
catch (...)
{
std::cout << "未知异常" << std::endl;
}
}
/* =============================================================== */
/*
* C# throw代码示例
*/
using System;
void DoSomething()
{
// 主动抛异常(必须抛 Exception 派生类)
throw new InvalidOperationException("恭喜你,又出错啦!");
}
void Test()
{
try
{
DoSomething();
}
catch (InvalidOperationException e)
{
Console.WriteLine("捕获到异常: " + e.Message);
}
catch (Exception)
{
Console.WriteLine("未知异常");
}
finally
{
Console.WriteLine("我很急,你赶快处理一下");
}
}
14. 🕳LINQ(语言集成查询 )
LINQ感觉就是C++中STL的再版本
C++ STL = 算法工具箱(要什么自己组合)
C# LINQ = 查询工具箱(语句)(链式组合,像写 SQL)
下面让ChatGPT帮忙列举了一下一些比较常用的,这个东西,还是随用随查,用多了,就记住了 才不是我记不住
功能
C++ STL(算法)
C# LINQ
过滤(筛选)
std::copy_if()
Where()
计数(带条件)
std::count_if()
Count(predicate)
判断是否存在
std::any_of()
Any()
判断是否全部符合
std::all_of()
All()
查找元素
std::find()
First() / FirstOrDefault()
查找符合条件的元素
std::find_if()
First(predicate)
遍历、映射(投影)
std::transform()
Select()
排序
std::sort()
OrderBy() / OrderByDescending()
去重
std::unique()(需排序)
Distinct()
求和
std::accumulate()
Sum()
最大值 / 最小值
std::max_element() / std::min_element()
Max() / Min()
截取前 N 个
std::copy_n()
Take()
跳过前 N 个
手写迭代器偏移
Skip()
拼接两个序列
std::copy() 合并到容器
Concat()
分组
手写 map<vector> 或 unordered_map<vector>
GroupBy()
统计(分组后)
手写循环
GroupBy().Select()
生成新集合
先创建 vector 后 push
Select().ToList()
延迟执行
只有 ranges::view 支持
所有 LINQ 查询默认延迟执行
提前执行(强制求值)
不存在该概念
ToList() / ToArray()
在此之前,我想单独讲讲链式表达式和查询表达式
看有老程序员说,链式表达式和查询表达式各有千秋,两种都有需要的场景
如果只是一些简单的运算,链式表达式比较好用
如果使用一些排序,查询,查询表达式比较好用
但是其他情况需要根据实际应用判断
奈何小登阅历尚浅,还是无法理解什么时候用链式,什么时候用查询
但是该讲的还是要讲
[!IMPORTANT]
链式表达式(Method Syntax)的语法
链式 = 连续调用扩展方法
核心规律:collection.Where(...).Select(...).OrderBy(...).ThenBy(...) ...
/* 链式表达式 */
var result = collection
.Where(x => 条件)
.Select(x => 投影)
.OrderBy(x => 排序键)
.ThenByDescending(x => 排序键)
.GroupBy(x => 分组键)
.Join(另一个集合, 条件, 投影)
...;
/* Demo */
var result = students
.Where(s => s.Age > 18)
.OrderBy(s => s.Name)
.Select(s => new { s.Name, s.Age });
/* 回顾 Lambda表达式 */
".Where(s => s.Age > 18)" 中 "s => s.Age > 18" 相当于
bool Filter(Student s)
{
return s.Age > 18;
}
[!IMPORTANT]
查询表达式(Query Syntax)的语法
查询表达式 = 类似 SQL 的语句
核心规律:from … in … where … select …
/* 查询表达式 */
var result =
from x in collection
where 条件
select 投影;
/* Demo */
var result =
from s in students
where s.Age > 18
orderby s.Name
select new { s.Name, s.Age };
给出一个教学视频,但是非常不适合初学者,不,应该说,完全不适合
但是,可以以后来看看,这一块我也不是学得特别明白,所以给自己挖个坑,以后再来将体系补充完善
以前学C / C++的时候,只写过数据库的查询语句,"SELECT image FROM XXX WHERE username = :username"
一开始觉得,两者好像有相似之处,现在——一个数据库,一个对象,哪里一样了,这么多年有没有好好学语法
对象查询,还是使用不熟练,哪怕看了很多教程,也是懵懵懂懂
LINQ入门示例及新手常犯的错误_哔哩哔哩_bilibili
15. 多线程
关于多线程编程,什么管道啊,子进程父进程啊,僵尸进程啊,我列一份目录出来
1.I/O文件操作:打开,关闭,读和写
2.父子进程,孤儿进程,僵尸进程
3. (1)管道(无名管道) [进程中加管道],无名管道的文件操作
(2)有名管道,有亲属关系的和无亲属关系的文件操作
(3)共享存储映射,创建与释放,读与写
(4)信号的基本了解,kil函数
4.线程
(1)线程
(2)互斥锁
(3)条件变量
哦,上面九成九的东西,我都不会讲,就是放出来让你看看
所以我们来聊聊怎么线程的创建,销毁,退出,读写锁什么的
只不过这里只能简单的讲一讲,毕竟细讲的话——别人视频都可以讲十几个小时,我一个刚转进来的何德何能可以一篇文章全部讲完
(1)线程的创建与销毁
解释一下这里的代码
创建两个线程,一个打印"Hello",一个打印"World",打印完成后退出线程
1)C++线程pthread和thread写法
/* pthread写法 */
// C 时代的写法,管你现在线程多么好用,没C时代的摸索,一切都是空气
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void printer(const char *str)
{
while (*str)
{
putchar(*str);
fflush(stdout);
str++;
// 让出CPU
sleep(1);
}
}
void *thread_func1(void *arg)
{
const char *str = "Hello";
printer(str);
// 线程退出
pthread_exit(NULL);
}
void *thread_func2(void *arg)
{
const char *str = "World";
printer(str);
// 线程退出
pthread_exit(NULL);
}
int main()
{
// 创建线程
pthread_t tid1;
pthread_create(&tid1, NULL, thread_func1, NULL);
pthread_t tid2;
pthread_create(&tid2, NULL, thread_func2, NULL);
while (1)
{
sleep(1);
}
return 0;
}
/* ================================================================= */
/* C++现代写法 - C++11等价写法 */
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void printer(const char* str)
{
while (*str)
{
putchar(*str);
fflush(stdout);
str++;
// 让出 CPU,让程序更容易交替输出
this_thread::sleep_for(chrono::seconds(1));
}
}
void thread_func1()
{
const char* str = "Hello";
printer(str);
}
void thread_func2()
{
const char* str = "World";
printer(str);
}
int main()
{
// 创建线程
thread t1(thread_func1);
thread t2(thread_func2);
t1.join(); // 等待线程结束
t2.join();
return 0;
}
C#线程Thread和Task写法
using System;
using System.Threading;
class Program
{
static void Printer(string str)
{
foreach (char c in str)
{
Console.Write(c);
Thread.Sleep(1000); // 让出CPU、模拟延迟
}
}
static void ThreadFunc1()
{
string str = "Hello";
Printer(str);
}
static void ThreadFunc2()
{
string str = "World";
Printer(str);
}
static void Main()
{
/* 1.创建线程 */
Thread t1 = new Thread(ThreadFunc1);
Thread t2 = new Thread(ThreadFunc2);
/* 2.线程开始 */
t1.Start();
t2.Start();
/* 3.等待线程结束 */
t1.Join();
t2.Join();
}
}
/* Task - 现代C#写法 */
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Printer(string str)
{
// 遍历
foreach (char c in str)
{
Console.Write(c);
Thread.Sleep(1000);
}
}
static async Task Main()
{
// 创建线程并且运行线程
// Task.Run(...) -> 把一个工作丢到线程池(系统调度的线程池线程)里异步执行
Task t1 = Task.Run(() => Printer("Hello"));
Task t2 = Task.Run(() => Printer("World"));
// 让当前方法(通常是 Main 或某个异步方法)暂停等待任务执行完
// 不会阻塞线程
await Task.WhenAll(t1, t2);
}
}
(2)锁
为什么需要锁,我们用一个C++代码示例、
假设最近本地来了一支乐队
3个狂热的乐队粉丝(3个线程)在同一时间开始抢票,接下来我们来看看他们的抢票结果
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
// 全局资源是共享的
// 票数:10
int g_count = 10;
// 狂热的乐队粉丝 -> 线程函数
void *thread_func1(void * arg)
{
// // 线程分离 -> 你要是闲得无聊可以加一句这个玩玩
// pthread_detach(pthread_self());
while (g_count > 0)
{
sleep(1);
// 下单
g_count--;
cout << "票数剩余: " << g_count << endl;
}
// 线程退出
pthread_exit(NULL);
}
void *thread_func2(void * arg)
{
while (g_count > 0)
{
sleep(1);
// 下单
g_count--;
cout << "票数剩余: " << g_count << endl;
}
// 线程退出
pthread_exit(NULL);
}
void *thread_func3(void * arg)
{
while (g_count > 0)
{
sleep(1);
// 下单
g_count--;
cout << "票数剩余: " << g_count << endl;
}
// 线程退出
pthread_exit(NULL);
}
int main()
{
pthread_t tid1;
pthread_create(&tid1, NULL, thread_func1, NULL);
pthread_t tid2;
pthread_create(&tid1, NULL, thread_func2, NULL);
pthread_t tid3;
pthread_create(&tid1, NULL, thread_func3, NULL);
while (1)
{
sleep(1);
}
return 0;
}
// -------------------------------------------------------------
输出结果:
票数剩余: 7
票数剩余: 7
票数剩余: 6
票数剩余: 5
票数剩余: 4
票数剩余: 3
票数剩余: 2
票数剩余: 1
票数剩余: 0
票数剩余: -1
票数剩余: -2
// 但是我们发现,票数居然为负数了,可是在明明设置了票数不能等于负数
// 原因很简单,线程在同一时间都觉得自己还能进 while,然后就出现了
// 线程 A:g_count-- → 0
// 线程 B:g_count-- → -1
// 线程 C:g_count-- → -2
// 如果避免这种情况发生?这个时候就需要锁这种东西了
1)C++锁
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 锁
pthread_mutex_t g_mutex;
// 票数:10
int g_count = 10;
void* func(void*)
{
while (true)
{
sleep(1);
// 上锁
pthread_mutex_lock(&g_mutex);
if (g_count <= 0)
{
pthread_mutex_unlock(&g_mutex);
break;
}
g_count--;
cout << "票数剩余: " << g_count << endl;
// 解锁
pthread_mutex_unlock(&g_mutex);
}
return nullptr;
}
int main()
{
// 创建3个粉丝 -> 3个线程
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, func, nullptr);
pthread_create(&t2, nullptr, func, nullptr);
pthread_create(&t3, nullptr, func, nullptr);
// 等待全部线程结束
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
return 0;
}
2)C#锁
// 古法炮制
using System;
using System.Threading;
class Program
{
// 锁对象
static readonly object locker = new object();
// 票数
static int g_count = 10;
static void Sell()
{
while (true)
{
Thread.Sleep(1000); // 模拟卖票时间
// lock(locker) { Todo.... }
// 大括号里面便是被锁住的内容,所以C#不需要再写锁和解锁
// 傻瓜式写法杜绝一切意外是吧
lock (locker)
{
if (g_count <= 0)
break;
g_count--;
Console.WriteLine($"票数剩余: {g_count}");
}
}
}
static void Main()
{
// 1.创建进程
Thread t1 = new Thread(Sell);
Thread t2 = new Thread(Sell);
Thread t3 = new Thread(Sell);
// 2.进程开始
t1.Start();
t2.Start();
t3.Start();
// 3.等待进程结束
t1.Join();
t2.Join();
t3.Join();
}
}
// ===========================================================
// Task + async
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static readonly object locker = new object();
static int g_count = 10;
static async Task SellAsync()
{
while (true)
{
await Task.Delay(1000);
lock (locker)
{
if (g_count <= 0)
break;
g_count--;
Console.WriteLine($"票数剩余: {g_count}");
}
}
}
static async Task Main()
{
Task t1 = SellAsync();
Task t2 = SellAsync();
Task t3 = SellAsync();
await Task.WhenAll(t1, t2, t3);
}
}
16. 🧨引用
这里我讲得很浅,只讲了this指针引用和普通引用的基础用法
主要是,我觉得C#中的引用相比C++的指针和引用,是真的过于——安全了
指针,引用的内容,我一直觉得就不是看书就可以看会的,必须被项目折磨才会明白
所以我就偷个懒,长话短说了
(1)this指针
this 只能在类(或结构体)内部使用,表示 当前对象本身
/* C++ */
class A {
public:
// 打印对象的真实内存地址
void Print() { cout << this << endl; }
};
/* --------------------------------------------------------------------- */
/* C# */
namespace Pro
{
class A
{
public void Print()
{
// 1.打印"类型名" - Pro.A
// 而不是地址
Console.WriteLine(this);
// 2.C# this 是引用,而不是指针
Console.WriteLine(this.ToString());
}
}
}
项目
C++ this
C# this
本质
指针
引用
是否真实内存地址
✔ 是
❌ 否
是否可取地址
✔ 是
❌ 不能(unsafe 才行)
是否能指针运算
✔ 可以
❌ 完全不行
是否能解引用
✔ 是
❌ 无意义
GC 追踪
❌ 无
✔ 有
安全性
较低
很高
常用场景
访问成员、返回自身、链式调用、判断对象地址
命名冲突、链式调用、传递当前对象
(2)引用
class Student { public int age; }
Student s1 = new Student();
Student s2 = s1; // 引用复制,不是深拷贝
s2.age = 18;
Console.WriteLine(s1.age); // 18
特性
C++ 引用
C++ 指针
C# 引用
本质
别名
地址变量
托管指针
可为 null
❌
✔
✔
可重新指向
❌
✔
✔
必须初始化
✔
❌
✔(默认 null)
是否能算术
❌
✔
❌
内存管理
手动
手动
GC 自动
典型场景
参数传递
链表/数组/底层
OOP 对象
安全性
中
低
高
易错点
悬空引用
悬空/越界
NRE 空引用
17.C#语言哲学 及 总结
查找了很多资料,但是我依然没有理解到C#的哲学核心是什么
没办法,才转入C#没多久,我实际上也就看了一个月不到的代码
别人哪怕说的再清楚明白,自己没有上手几次,也是空谈
在我查到的资料中,大概只有这一句话最符合我现在的认知
C#旨在 让开发者把精力放在业务价值,而不是对抗语言复杂性
所以我认为,C#是面向业务的,以对象面向业务
C#是面向业务的编程语言
以简化逻辑为主,优化业务为主
尽量保持零成本抽象的同时,做到渐进式增强
但是,这些感悟我也只是道听途说,还没有完全去理解它
或许今后的某一天我会突然领悟,但是也至少的许多月之后了,
没有大大小小项目的实战,我又不是天赋狗,不可能领悟那么快的
除此之外,学完了C#基本语法之后,让我觉得C++这门语言是越学越复杂, C#:不对,有牛
C#早期的基本语法参考了大量C++语法,但是又对其做出了大量的功能优化和使用优化,
很大程度上减少了相同业务所需的代码量
在学习C#的语法过程中,我使用了大量的C++语法作为参考,
越发觉得我不过是会些皮毛,什么举一反三,我觉得我还没有到那种程度
我只能调侃几句:需要举一反三,那是因为你写的还不够多啊😋
同时,我也意识到一件事情,网络上的资源,似乎已经满足不了我对一些知识点的探究,以及对个人体系的组建了,
不是我好学,是因为,压根找不到 —— 你知道一个强迫症患者写的笔记里面缺失一块是多么难受吗
早期带我入门C++的老师曾经说过,后面的提升只能依靠书籍,国外的文献以及程序员之间的经验分享
这句话当时没放在心上,现在觉得非常有道理,
其次,就是不要过于去纠结某一个语法,那会浪费你很多的时间,不如将问题记下来
当你在实际开发中遇见那个问题的时候,你会豁然——你会被折磨n久之后豁然开朗
过于纠结语法的例子比比皆是,国内一个强弱类型都可以让一堆人吵起来
上次看有一个up说C++没有内存模型,评论区吵了不知道多少楼,
然后还有一群吃瓜群众乱入,嚷嚷着,打起来打起来,最喜欢看谭浩强理论篇和实践派打起来了
此篇总结只是根据个人的理解,然后在网上翻找大量的教程写下的总结,
很多大佬他们能力虽然特别强,但是是真的不适合教人啊
实在不会的语法或者不熟悉的语法,我都是逐行拆解,一句一句理解的
所以也比较适合初学者,但是更加适合有过C/C++开发经验的开发人员
我也不太清楚,我能在.net这条路走多久,虽然毕业之后没走上C++的开发
但是C++的学习,我也不会放弃,我还是非常喜欢C++的开发的
因为足够自由,但是自由的代价就是你得学会很多东西,足够强大
C#里面很多精简的语法我也相当喜欢,特别是万能的"=>"
还是老样子,最后再说几句,这篇笔记是根据网上各种各样网站的教程,再结合一部分大佬的博客
最后结合那微不足道的个人经验总结的,如有错误,欢迎指出
