c++程序员快速了解C#
笔者最近从自研游戏引擎转向了商业游戏引擎,unity和UE都会涉及,目前是先帮一个新立项的unity项目做东西。编程语言需要从C++切换到C#。这篇文章是笔者对照C++了解C#的总结,一些不重要的或显而易见的就省略了,对同样从事C++,想要快速了解C#的可能有一点参考作用,想要系统性学习的还是找本书自己看比较好。
笔者主要参考的资料是C# 教程 - 概述、C# 教程 | 菜鸟教程。
概述#
C# 程序在 .NET 上运行,而 .NET 是名为公共语言运行时 (CLR) 的虚执行系统和一组类库。 CLR 是 Microsoft 对公共语言基础结构 (CLI) 国际标准的实现。 CLI 是创建执行和开发环境的基础,语言和库可以在其中无缝地协同工作。
用 C# 编写的源代码被编译成符合 CLI 规范的中间语言 (IL)。 IL 代码和资源(如位图和字符串)存储在扩展名通常为 .dll 的程序集中。 程序集包含一个介绍程序集的类型、版本和区域性的清单。
执行 C# 程序时,程序集将加载到 CLR。 CLR 会直接执行实时 (JIT) 编译,将 IL 代码转换成本机指令。 CLR 可提供其他与自动垃圾回收、异常处理和资源管理相关的服务。 CLR 执行的代码有时称为“托管代码”。而“非托管代码”被编译成面向特定平台的本机语言。
C#的一些特点:
- C# 是面向对象的、面向组件的编程语言
- *垃圾回收*自动回收不可访问的未用对象所占用的内存
- 可以为 null 的类型可防范不引用已分配对象的变量
- 统一类型系统,所有C#类型均继承自object类型
- 所有类型共用一组通用运算
- 任何类型的值可以一致性地存储、传输和处理
- 支持用户定义的引用类型和值类型
- 允许动态分配轻型结果的对象和内嵌存储
- 支持泛型方法和类型
- 提供迭代器
类型系统#
值类型#
-
简单类型
- 有符号整形:sbyte、short、int、long
- 无符号整形:byte、ushort、uint、ulong
- Unicode字符:char,表示UTF-16代码单元
- IEEE二进制浮点:float、double
- 高精度十进制浮点数:decimal
- 枚举类型:enum
- 结构类型:struct
- 可以位null的值类型
- 元组值类型
还有一些细节需要注意:
整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,没有前缀则表示十进制。整数常量也可以有后缀,可以是 U 和 L 的组合,其中,U 和 L 分别表示 unsigned 和 long。后缀可以是大写或者小写,多个后缀以任意顺序进行组合。
使用浮点形式表示时,必须包含小数点、指数或同时包含两者。使用指数形式表示时,必须包含整数部分、小数部分或同时包含两者。有符号的指数是用 e 或 E 表示的。
引用类型#
-
类类型
- 所有类型的最终基类:object
- dynamic,可以存储任何类型的值在动态数据类型变量中,类型检查在运行时发生。写法:dynamic = value
- Unicode字符串:string(是System.String的别名),加上@之后表示字符串里的字符就是最终字符,不需要再转义了
- 用户自定义的class类型
- 接口类型:interface
- 数组类型:int[], int [,] int
- 委托类型:delegate
用户可定义以下六种 C# 类型:类类型、结构类型、接口类型、枚举类型、委托类型和元组值类型。
delegate类型表示引用包含特定参数列表和返回类型的方法。通过委托,可以将方法视为可分配给变量并可作为参数传递的实体。委托类同于函数式语言提供的函数类型。他们还类似于其他一些语言中存在的函数指针概念,但是它是面向对象且类型安全的。
元组:提供简洁的语法将多个数据元素分成一个轻型数据结构。
(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
//Output:
//Sum of 3 elements is 4.5.
可空类型#
C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值。
int? num1 = null;
int? num2 = 45;
double? num3 = new double?();
double? num4 = 3.14157;
bool? boolval = new bool?();
// 显示值
Console.WriteLine("显示可空类型的值: {0}, {1}, {2}, {3}",
num1, num2, num3, num4);
Console.WriteLine("一个可空的布尔值: {0}", boolval);
Console.ReadLine();
// 验证
76505023-a3c1-4725-b353-6ad77cc4be2d
程序结构#
包含:命名空间、类型、成员、程序集。
编译完的C#程序会打包到程序集中。程序集的文件扩展名为.exe或.dll。
由于程序集是包含代码和元数据的自描述功能单元,因此无需在C#中使用include,只需在编译程序时引用特定的程序集,即可使用此程序集中包含的公共类型和成员。
namespace Acme.Collections;
public class Stack<T>
{
Entry _top;
public void Push(T data)
{
_top = new Entry(_top, data);
}
public T Pop()
{
if (_top == null)
{
throw new InvalidOperationException();
}
T result = _top.Data;
_top = _top.Next;
return result;
}
class Entry
{
public Entry Next { get; set; }
public T Data { get; set; }
public Entry(Entry next, T data)
{
Next = next;
Data = data;
}
}
}
程序内容#
访问限制#
public
:访问不受限制。
private
:访问仅限于此类。
protected
:访问仅限于此类或派生自此类的类。
internal
:仅可访问当前程序集(.exe
或.dll
)。
protected internal
:仅可访问此类、从此类中派生的类,或者同一程序集中的类。
private protected
:仅可访问此类或同一程序集中从此类中派生的类。
方法#
静态方法是通过类进行访问,实例方法是通过类实例进行访问。
在声明方法的类中,方法的签名必须是唯一的。方法签名包含方法名称、类型参数数量及其参数的数量、修饰符和类型。 方法签名不包含返回类型。
当方法主体是单个表达式时,可使用紧凑表达式格式定义方法,如下例中所示:
public override string ToString() => "This is an object";
参数#
分为值参数、引用参数、输出参数和参数数组。
- 引用参数 修饰符 ref
- 输出参数 修饰符 out,不要求给自变量显式赋值
- 参数数组允许向方法传递数量不定的自变量。使用 params修饰。只能是最后一个参数,必须是以为数组类型。可以通过传递性参数组的元素类型的任意数量实参来达到相同的效果,这种情况参数数组实例会自动创建,并用传递的自变量初始化。
public class Console
{
public static void Write(string fmt, params object[] args) { }
public static void WriteLine(string fmt, params object[] args) { }
// ...
}
int x, y, z;
x = 3;
y = 4;
z = 5;
Console.WriteLine("x={0} y={1} z={2}", x, y, z);
//等价
int x = 3, y = 4, z = 5;
string s = "x={0} y={1} z={2}";
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);
局部变量#
C#要求必须显式赋值局部变量,才能够获取其值。
虚方法、重写方法和抽象方法#
- 虚方法是在基类中声明和实现的方法,任何派生类都可提供更具体的实现
- 重写方法是在派生类中实现的方法,可修改基类的实现
- 抽象方法是在基类中生命的方法,必须在派生类中重写
- 调用虚方法时,运行时决定了要调用的实际方法的代码;调用非虚方法时,编译时决定
- 如果方法声明中有override修饰符,那么可以在派生类中重写虚方法
- 抽象方法是没有实现的虚方法,用abstract修饰符声明,仅可在抽象类中使用
-
同一个类中的方法同名,但是签名不同,就是重载
virtual和abstract的区别
在C#中virtual和abstract两者是都为了让子类中重新定义来覆盖父类的定义。
- virtual(虚方法)或者(abstract)抽象方法是不能私有的,二者中private成员是不能被子类访问的
- virtual可以被子类重写,abstract必须被子类重写
- 如果重写了virtual,子类方法中必须用override来实现方法的重写
- 如果类成员被abstract修饰,那么该类必须也添加abstract。抽象类才有抽象方法
接口与抽象类的区别
接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。
抽象类在某种程度上与接口类似,但是它们大多只用在只有少数方法由基类声明由派生类实现时。
其他函数成员#
- 构造函数
- 属性
- 索引器
- 事件
- 运算符
- 终结器(析构函数)
示例如下
// 构造函数
public MyList(int capacity = DefaultCapacity)
{
_items = new T[capacity];
}
// 终结器
~public MyList()
{
// clear.
}
// 索引器
public T this[int index]
{
get => _items[index];
set
{
_items[index] = value;
OnChanged();
}
}
// 事件
protected virtual void OnChanged() =>
Changed?.Invoke(this, EventArgs.Empty);
// 运算符
public static bool operator ==(MyList<T> a, MyList<T> b) =>
Equals(a, b);
// 终结器示例
class EventExample
{
static int s_changeCount;
static void ListChanged(object sender, EventArgs e)
{
s_changeCount++;
}
public static void Usage()
{
var names = new MyList<string>();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(s_changeCount); // "3"
}
}
集合类型#
数组#
int[] a1 = new int[10];
int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];
int[][] a = new int[3][];
a[0] = new int[10];
a[1] = new int[5];
a[2] = new int[20];
int[] a = new int[] { 1, 2, 3 };
int[] a = { 1, 2, 3 };
// 遍历
for (int i = 0; i < a.Length; i++)
{
Console.WriteLine($"a[{i}] = {a[i]}");
}
foreach (int item in a)
{
Console.WriteLine(item);
}
字符串插值(String interpolation)#
String interpolation通过$来声明。字符串内插计算{}之间的表达式,将结果转换为string,并将括号内的文本替换为表达式的字符串结果。
Console.WriteLine($"The low and high temperature on {weatherData.Date:MM-DD-YYYY}");
Console.WriteLine($" was {weatherData.LowTemp} and {weatherData.HighTemp}.");
// Output (similar to):
// The low and high temperature on 08-11-2020
// was 5 and 30.
委托类型#
C# 中的Delegate类似于 C 或 C++ 中函数的指针。 是存有对某个方法的引用的一种引用型变量,引用可在运行时被改变。
Delegate特别用于实现事件和回调方法。所有的Delegate都派生自 System.Delegate
类。
声明Delegate:
delegate <return type> <delegate-name> <parameter list>
public delegate int MyDelegate (string s);
实例化Delegate需要用new创建一个Delegate,参数是Delegate同签名的函数。
delegate int NumberChanger(int n);
// 创建委托实例
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
// 使用委托对象调用方法
nc1(25);
声明个委托类型的函数签名,当委托作为参数时,可以接收相同签名的函数。
delegate double Function(double x);
class Multiplier
{
double _factor;
public Multiplier(double factor) => _factor = factor;
public double Multiply(double x) => x * _factor;
}
class DelegateExample
{
static double[] Apply(double[] a, Function f)
{
var result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
public static void Main()
{
double[] a = { 0.0, 0.5, 1.0 };
double[] squares = Apply(a, (x) => x * x);
double[] sines = Apply(a, Math.Sin);
Multiplier m = new(2.0);
double[] doubles = Apply(a, m.Multiply);
}
}
还可以使用匿名函数或Lambda表达式创建委托,这些函数是在声明时创建的“内联方法”。匿名函数可以查看周围方法的局部变量。
double[] doubles = Apply(a, (double x) => x * 2.0);
异步程序关键字async和await#
public async Task<int> RetrieveDocsHomePage()
{
var client = new HttpClient();
byte[] content = await client.GetByteArrayAsync("https://docs.microsoft.com/");
Console.WriteLine($"{nameof(RetrieveDocsHomePage)}: Finished downloading.");
return content.Length;
}
- 方法声明包含
async
修饰符,声明这是异步方法
await
等待GetByteArrayAsync
方法的返回
return
语句中指定的类型与方法的Task<T>
声明中的类型参数匹配。 (返回Task
的方法将使用不带任何参数的return
语句)
其他#
正则:Regex
string input = "1851 1999 1950 1905 2003";
string pattern = @"(?<=19)\d{2}\b";
foreach (Match match in Regex.Matches(input, pattern))
Console.WriteLine(match.Value);
异常:try catch
文件的输入出处:System.IO、 FileStream
高级特性#
Atrribute特性#
Attribute是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。
Attribute用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。
预定义特性:
- Conditinonal,标记了一个条件方法,其是否执行取决于标志符。在下例中,没有define TEST就不会执行这个函数
public class Myclass
{
[Conditional("TEST")]
public static void Message(string msg)
{
Console.WriteLine(msg);
}
}
-
Obsolete,标记了不应被使用的程序实体,参数如下
- message,描述字符串
- iserror,true则编译器报错,false则编译器输出警告
using System;
public class MyClass
{
[Obsolete("Don't use OldMethod, use NewMethod instead", true)]
static void OldMethod()
{
Console.WriteLine("It is the old method");
}
static void NewMethod()
{
Console.WriteLine("It is the new method");
}
public static void Main()
{
OldMethod();
}
}
自定义Attribute,见C# 特性(Attribute) | 菜鸟教程 。
反射#
反射能完成运行时查看程序集中每一个类型的的属性、方法、字段等信息。
从别处摘下来的C#反射的有用途:
- 使用Assembly定义和加载程序集,加载在程序集清单中列出模块,以及从此程序集中查找类型并创建该类型的实例。
- 使用Module了解包含模块的程序集以及模块中的类等,还可以获取在模块上定义的所有全局方法或其他特定的非全局方法
- 使用ConstructorInfo了解构造函数的名称、参数、访问修饰符(如pulic 或private)和实现详细信息(如abstract或virtual)等
- 使用MethodInfo了解方法的名称、返回类型、参数、访问修饰符(如pulic 或private)和实现详细信息(如abstract或virtual)等
- 使用FiedInfo了解字段的名称、访问修饰符(如public或private)和实现详细信息(如static)等,并获取或设置字段值
- 使用EventInfo了解事件的名称、事件处理程序数据类型、自定义属性、声明类型和反射类型等,添加或移除事件处理程序
- 使用PropertyInfo了解属性的名称、数据类型、声明类型、反射类型和只读或可写状态等,获取或设置属性值
- 使用ParameterInfo了解参数的名称、数据类型、是输入参数还是输出参数,以及参数在方法签名中的位置等
反射用到的主要类:
- System.Type,通过这个类可访问任何给定数据类型的信息
- System.Reflection.Assembly,它可以用于访问给定程序集的信息或者把这个程序集加载到程序中
Type类见Type Class 。
Type t = typeof(string);
string s = "grayworm";
Type t = s.GetType();
Type t = Type.GetType("System.String");
下面是一个简单的反射示例。
using AssemblyTest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace AssemblyTest
{
public class Test
{
/// <summary>
/// 获取程序集信息
/// </summary>
public void GetType()
{
Type type = typeof(THelper);//获取Type
Console.WriteLine("程序集名称:" + type.Assembly.GetName().Name);
Console.WriteLine("命名空间名称:" + type.Namespace);
Console.WriteLine("类名称:" + type.Name);
Console.WriteLine("类全名(包含命名空间):" + type.FullName);
Console.WriteLine("类基类:" + type.BaseType.Name);
Console.WriteLine("程序集位置:" + type.Assembly.Location);
Console.Write("{0}类的全部方法:", type.Name);
MethodInfo[] mathod = type.GetMethods();
foreach (var item in mathod)
{
Console.Write(item.Name + ",");
}
Console.WriteLine();
//*******下面是通过.DLL文件动态加载程序集***********
//动态加载程序集(括号里写程序集名称)
//Assembly assembly = Assembly.Load("AssemblyTest");
//这里调用的是生成的AssemblyTest.exe文件,如果是DLL文件,就改为.DLL
Assembly assembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + "AssemblyTest.exe");
//注意:括号里写(程序集名称.类名)
Type types = assembly.GetType("AssemblyTest.THelper");
//获取方法
MethodInfo method = types.GetMethod("MethodTest");
//获取方法参数
Console.WriteLine("MethodTest方法的参数:" + method.GetParameters());
//创建对象实例
object instance = System.Activator.CreateInstance(types);
//通过Invoke调用,返回执行返回结果
object result = method.Invoke(instance, new object[] { "王建" });
Console.WriteLine(result);
}
}
}
属性#
get、set,给外面提供访问类内部私有成员变量的途径。
// 声明类型为 int 的 Age 属性
public int Age
{
private int age;
get
{
return age;
}
set
{
age = value;
}
}
索引器#
用法如下,索引器可以被重载。
element-type this[int index]
{
get{}
set{}
}
泛型#
有点像C++模板,但没有C++模板复杂,主要区别如下(摘自微软官方文档):
- C# 泛型的灵活性与 C++ 模板不同。 例如,虽然可以调用 C# 泛型类中的用户定义的运算符,但是无法调用算术运算符
- C# 不允许使用非类型模板参数,如
template C<int i> {}
- C# 不支持显式定制化;即特定类型模板的自定义实现
- C# 不支持部分定制化:部分类型参数的自定义实现
- C# 不允许将类型参数用作泛型类型的基类
- C# 不允许类型参数具有默认类型
- 在 C# 中,泛型类型参数本身不能是泛型,但是构造类型可以用作泛型。 C++ 允许使用模板参数
- C++ 允许在模板中使用可能并非对所有类型参数有效的代码,随后针对用作类型参数的特定类型检查此代码。 C# 要求类中编写的代码可处理满足约束的任何类型。 例如,在 C++ 中可以编写一个函数,此函数对类型参数的对象使用算术运算符
+
和-
,在实例化具有不支持这些运算符的类型的模板时,此函数将产生错误。 C# 不允许此操作;唯一允许的语言构造是可以从约束中推断出来的构造
static void Swap<T>(ref T lhs, ref T rhs)
{
T temp;
temp = lhs;
lhs = rhs;
rhs = temp;
}
匿名方法#
匿名方法提供了一种传递代码块作为delegate参数的技术,它是没有名称只有主体的方法。匿名方法不需要指定返回类型,它是从方法主体内的return语句推断的。
匿名函数写法:
delegate void NumberChanger(int n);
NumberChanger nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};
线程#
见System.Threading.Thread类。
public static void CallToChildThread()
{
Console.WriteLine("Child thread starts");
// 线程暂停 5000 毫秒
int sleepfor = 5000;
Console.WriteLine("Child Thread Paused for {0} seconds",
sleepfor / 1000);
Thread.Sleep(sleepfor);
Console.WriteLine("Child thread resumes");
}
static void Main(string[] args)
{
ThreadStart childref = new ThreadStart(CallToChildThread);
Console.WriteLine("In Main: Creating the Child thread");
Thread childThread = new Thread(childref);
childThread.Start();
Console.ReadKey();
}
C#与C++的主要区别#
特性 | C# | C++ |
---|---|---|
编译 | 将代码编译成基于CLR的中间语言,再由CLR实时编译成机器码 | 将代码编译成机器码 |
内存管理 | 由垃圾回收器管理 | 由程序员自行管理 |
平台依赖 | 基于windows | 可以在任何平台运行 |
边界检查 | 由编译器作边界检查 | 不做边界检查 |
指针 | 只能在unsafe模式下使用 | 可以任意使用 |
语言类型 | 高级面向兑现该语言 | 低级语言 |
面向对象 | 纯面向对象的编程语言 | 由于原始数据类型,C++不是纯面向对象的编程语言 |
访问修饰符 | public、private、protected、internal、protected internal | public、private、protected、 |
switch | 测试变量可以是字符串 | 不能是字符串 |
函数指针 | 无函数指针概念,但有delegate | 有函数指针概念 |
二进制文件 | 由于库的开销,二进制文件比较大 | 二进制比较小 |
项目类型 | 应用于现代程序开发 | 应用于注重访问硬件和更好性能的项目 |
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示