本篇文章的核心在于通过比较C#与C++的不同来快速上手C#。
一.变量类型
1.值类型:int,double,float,bool等都与c++相同,值类型可以赋值为空null,不过这里需要加上?(如:int? i = null;)称为可空值类型。
2.引用类型:如class string interface delegate record等,存储的是数据的引用,两个变量可以控制同一对象,引用类型可以引用一个空对象null。
3.一般局部变量(方法里的临时变量)都是存放在栈区,而引用类型一般都是存放在堆区。
4.对于数组,C#定义的方式与C++不同,与java相似(如:int[] a =new int[10];也可以简写为int[] a =[10];),二维数组:int[,] a = { { },{ } },数组也属于引用类型。
5.装箱 :将值类型转化为引用类型,装箱的本质是在堆上创建一个对象,把值类型的数据打包装进去,并返回一个引用,任何类型都直接或间接的派生自object类,所以任何值类型都可以隐式转化为object类型,装箱是在堆上创建了值类型的副本,返回的是副本的引用,无法通过装箱返回的引用来改变原始数据的值,拆箱就是装箱的逆过程,将引用类型再转化为值类型,但是拆箱的结果必须是原类型,否则会抛出异常,拆箱的语法与强制类型转化相同,装箱的语法与C++引用类似。
6.is与as :如bool b=obj is int;判断某个对象是否是指定类型,并返回bool值,一般用于引用转化,装箱,拆箱;as尝试把对象转换为指定的引用类型或可空类型,成功返回该类型对象,失败返回null
as可用于引用转换和装箱转换,不能用于值类型转换(因此不可用作拆箱),is与as都不能用于自定义类型。
注意与c++不同的地方:
1.float类型的数据赋值时数据后面必须加上f,如float a=8.8;(错误,直接无法编译通过), 应该为float a=8.8F(浮点数自动解释为double类型);
2.c#存在decimal类型用于精确存储浮点数,我们都知道,使用float与double存储的浮点数是近似值,demical类型可以以十进制的形式准确存储浮点数,但其计算效率相对较低,所以凡是涉及金钱、财务、税率等对精度要求极高的场景,必须使用 decimal;而在科学计算、图形渲染等对性能要求高且允许微小误差的场景,则使用 double。
3.c#中的自动类型推导是关键字var
二.方法
1.C#不允许全局方法。
2.表达式主体:如( public void show()=> Console.WriteLine(age);),可用于简化一些简单的方法。
3.对于C#方法的参数传递,存在以下几个关键字:ref(类似于C++的&引用,表示以引用方式传参,一般用向方法内部传递变量,向内传递一定要在方法外进行初始化),in(相当于const,同样是引用传递,以只读方式传参,in关键字在传递时可以不加),out(用于输出参数,同样可以看成是引用,不过与ref不同的是它一般用于向方法外传递变量,可以不在方法外初始化,在方法内进行初始化,由于次特性,我们可以在传递参数的同时进行声明即:test(out var f)),使用这些关键字传递参数,我们在调用方法传参时与方法的形参列表中都要在变量的前面加上。
4.C#方法的可变参:如:int test(params int[] number),相当于接收了一个int类型的动态数组。需要注意的是,这里如果存在多个参数,那么参数数组必须放在最后一个参数的位置。
常见方法:
1.打印/输出方法,Console类的Write方法(不换行)与WriteLine方法(自动换行)
占位输入 :(1):Console.WriteLine("{a}+{b}={result}") (一般都使用第一种),**左对齐与右对齐**:Console.WriteLine("{a,-10}+{b}={result}")(左对齐需要加上-号,后面数字表示预留空间)
(2):Console.WriteLine("{0}+{1}={2}",a,b,result)
2.输入方法
Console类的ReadLine方法,返回值为string类型,一般需要通过int.parse等方法进行类型转化。如int result = int.Parse(Console.ReadLine());
2.数组方法
GetLength方法,传入0计算二维数组的行数/一维数组的元素个数,1计算列数,
三.类
1.与java相同C#的一个类一般放在一个单独的文件中。
2.c#类内定义的静态成员变量可以直接在类内初始化,而C++需要在类内声明,在类外初始化,访问方式上c#访问静态变量与访问普通变量相同都使用 "." ,而C++中需要使用类名加作用域操作符"::"。
3.与C++相同,类内定义的静态成员方法只能使用静态成员变
4.类的实例化方式与java相同,都必须使用 Test t = new Test ();(也可以简写为Test t = new ()),原因在于C#自动实现底层内存的管理,大部分变量,类都是直接开辟到堆区的,自动释放,而C++更为灵活,由程序员自己控制堆区内存的开辟与释放。
5.C#保留了栈上分配 的能力,主要通过struct来实现,不同于C++,C#中的struct与class存在很大差别,比如不能有默认的无参构造函数,不能被继承等,C#中的struct不是类,而是值类型,C#中的class与struct是没有默认的访问限定符的,C#中的struct一般用于存放一组相关的数据成员,也存在构造函数,字段,属性,方法,使用结构体必须初始化全部的成员变量,无论有没有手动提供构造函数,编译器都会为结构体提供一个默认构造函数。
6.c#中不允许使用C++样的public:,只能使用类似java的单独指定访问限定符。
7.C#中的属性 与C++略有不同:C#的属性包括字段(后备字段)与方法两部分,与C++相同,为了提高函数的封装性,C#同样需要将属性设置为私有,但是与C++不同的是,我们写的private int age;只是属性的字段,属性还包括get与set访问器(本质是方法)来控制对于私有字段的读写。如图:
cs
class Test
{
private int _age;
public int Age
{
get{ return _age; }
set { _age = value; }
}
}
但是我们看到,这样写的话稍微有些复杂,所以我们需要使用自动属性 :public int Age{get;set;},编译器会自动的为我们生成字段,但是这样与单单写一个public的字段没有什么区别,所以为了代码的封装性,我们一般写成public int Age{get;private set;},来防止外部随意修改,但是对于一些一旦确定就无需修改的成员变量,我们这样写可以防止内外修改,却没办法防止类内修改,所以对于这种情况,我们需要一个新的访问器:init(它表示的是我们只能在构造函数与初始化阶段对其进行初始化,这之后无论类内,类外都无再修改)
8.我们实例化一个对象后可以使用对象的初始化语句进行快速初始化,并且其属于初始化的一部分,可以对于init的字段进行初始化,同时注意,对象的初始化语句运行是在构造函数之后的,如:
cs
class Test
{
public int Age{get;init;}
}
//....
Test t=new();
{
Age=10;
}
9.const,readonly,const与C++相同,一旦声明必须在声明同时初始化,编译时期确定一个初始化无法改变,readonly用于修饰字段,我们可以在声明时不进行初始化,在构造函数中对其进行初始化,经过这次初始化后无法再进行修改,是运行时常量,一般对于类内的字段我们都将其设置为readonly。
10.C#中的this不再是指针,而是引用。
11.由于C#的内存管理方式与C++不同,存在垃圾回收机制,所以绝大部分情况下,我们不需要手动的对资源进行回收释放,即不需要手动写析构函数。
四.语句
1.C#的switch case语句,为了安全考虑,在每一个case后面必须加上break,否则编译就无法通过,除非这里的case1中不存在任何内容。
2.C#的foreach语句,如foreach(var v in arr),相较于C++,关键字变为了foreach,中间变成了in
3.lambda表达式,与C++相同C#也存在lambda表达式,不过结构上存在不同,基本模式是:(输入参数) => 表达式或语句块,注意:无参数:() 不能省略,单个参数:括号 () 可以省略(这是最简洁的写法),多个参数:必须使用括号 () 包裹,多行语句块:如果逻辑比较复杂,需要用大括号 {} 包裹,并显式使用 return。
五.继承与多态
1.C#不允许多重继承,但C++允许。
2.C#中继承相较于C++不需要写访问限定符。
3.对于继承中如果父类存在构造函数,子类需要调用父类的构造函数,我们可以使用base关键字,来调用父类的成员,最常见的就是调用父类的构造函数,base()。
4.C#中存在object类,它是所以类型的基类。
5.sealed关键字:用于阻止继承,被sealed修饰的类无法被继承,修饰的方法无法在子类中重写。
6.C#中存在6种访问限定符,public,private,protected与C++基本相同,internal:可在当前程序集中访问,protected internal:可在当前类或派生类,或者当前程序集的任何联系访问,private internal :仅限当前程序集定义该成员的类或派生类可访问,其中public与internal可以直接修饰顶级类型(类型分为两种顶级与嵌套)。
7.与C++类似C#中也存在虚方法与方法的重写,覆盖与隐藏,不同的是C#子类重写父类中的虚方法,需要在子类重写方法前添加override关键字。
8.C#中同样存在抽象类,抽象类专门用于继承不可被实例化,其实现不同于C++,如果我们想要实现一个抽象类,需要将需要重写的父类方法的virtual关键字改为abstract关键字,将其由虚方法变为抽象方法,抽象方法没有函数体,不需要像C++一样写=0,存在抽象方法的类就变为了抽象类,抽象类必须在类的最前面加上abstract关键字。
9.C#中存在接口,需要在前面加上interface关键字,一般命名需要在名称前面加上一个大写的I,接口 最核心的作用就是"定义契约"。它只规定了一组方法、属性或事件的签名(即"能做什么"),但不提供任何具体的实现(即"怎么做")。任何类或结构体只要实现了这个接口,就必须严格履行这份契约,接口默认是public的,一般不加访问限定符,类实现接口的语法与继承相同。
接口有一种类似泛型的思想:比如我们想要使用同一个方法遍历几种不同的可枚举类型,我们可以将方法的形参改为IEnumberable(接收所以可枚举类型),来实现C++中需要模版才能实现的效果。
10.C#中同样存在运算符重载,运算符重载必须是public static的,C#中,二元运算符必须接收两个参数,特别的C#存在自定义的类型转化,我们可以自定义显示(public static explicit operator)与隐式(public static implicit operator)的类型转化。
六.委托与事件
如:public delegate int MyDelegate(int a,int b);作用有些类似于C++的functional或者函数指针,委托 本质是也是一个类,需要delegate关键字,可以使用一个方法或者成员方法对其进行初始化,如:MyDelegate del=Calculator.Add;(假设我们这里存在Calculator类,类内存在Add方法与Sub方法),我们就使用MyDelegate对Add成员方法进行了委托,应该委托可以委托多个方法(称为多播委托),如我们再写:del+=calc.Sub()(这里Add是静态方法,Sub是普通方法,所以Sub还需要依赖于对象,calc是我们创建的一个对象),委托多个方法我们如果调用的话会根据委托顺序依次调用各个方法(我们也可以使用-=号取消委托),对于多播委托,我们调用方法的返回值固定是最后一个方法的返回值。
事件 event为对象之间提供了一种通信方式,它们基于发布者-订阅者模型:当特定事情发生时,发布者会通知其他订阅者,事件是基于委托的封装,我们应该使用标准事件模型而非自定义事件,如:public event MyDelegate MyEvent;事件必须依赖于委托存在,需要event关键字,事件在使用时需要触发,如:MyEvent?.Invoke(int a,int b);
七.泛型
C#中的泛型类与泛型方法与C++中的类模版与函数模版类似。Action泛型,是一个泛型委托,无返回值,Func同样是一个泛型委托,存在返回值,Predicate谓词,只接受一个参数,返回值固定为bool。
泛型约束:在定义的泛型类或者泛型方法后面加上where T :struct(struct表示限制T为非空值类型),我们还可以使用class(限制T为引用类型),new()(限制T必须存在public的无参构造函数,与其他约束组合时必须放在最后),基类名称(限制T必须继承自该基类)
泛型集合:类似于C++容器,List<T>与C++的vector类似(添加成员方法Add,删除方法Remove,count方法与C++size相同),都是动态数组,Dictionary<key,value>相当于C++的unordered_map,Queue<T>与C++queue相同,Stack<T>与C++的stack相同。
八.异步编程
C#中的异步编程与C++中的多线程存在相似,却又有着很大不同,C#中异步的目的与C++多线程相同,都是为了提高运行效率,但是异步的核心目的是为了防止程序在遇到耗时操作时导致程序阻塞,多线程的目的是为了实现并行,C#的异步不一定是多线程,其本质的并行(轮流运行),表现的像并发。
C#中的异步是在遇到第一个 await 关键字之前的所有代码都在同一个线程运行,当程序遇到await 一个耗时操作时,当前线程会被立即释放,去处理其他任务,当这个耗时操作完成后,程序需要运行后面的代码,但不一定会回到原来的那个线程。
C#异步编程的核心是async与await关键字,async关键字的作用是声明一个方法为异步方法,其方法体内包含await,await的作用是在先暂停这个方法,立即释放这个线程,去处理其他任务,当等待完成后再在线程池中取一个线程去完成接下来的任务。
C#中也存在多线程,异步本质与多线程是不同的东西,C++中也存在类似异步的方法。
如,我们举一个经典例子:假设我们想要执行:点外卖-》等外卖-》吃外卖-》学习C#这一流程,同步方法的代码如图:
cs
static void OrderDelivery()
{
Console.WriteLine("ordering delivery");
}
static void WaitDelivery()
{
Console.WriteLine("waiting delivery...");
sleep(5000);
Console.WriteLine("delivery arrived");
}
static void EatDelivery()
{
Console.WriteLine("eat delivery...");
sleep(5000);
Console.WriteLine("finish eating");
}
static void LearnCsharp()
{
Console.WriteLine("learning C-sharp...");
sleep(5000);
Console.WriteLine("finish learning");
}
static async Task Main(string[] args)
{
var p= Stopwatch.StartNew();//记录运行时间的方法
OrderDelivery();
WaitDelivery();
EatDelivery();
LearnCsharp();
p.Stop();
Console.WriteLine(p.Elapsed.TotalSeconds);
}
那么这个程序最后的运行时间就是20秒,但是我们如果采用异步的方法就可以在等外卖与吃外卖的过程中学习C#,提高程序运行的效率,如图:
cs
static void OrderDelivery()
{
Console.WriteLine("ordering delivery");
}
static async Task WaitDelivery()
{
Console.WriteLine("waiting delivery...");
await Task.Delay(5000);
Console.WriteLine("delivery arrived");
}
static async Task EatDelivery()
{
Console.WriteLine("eat delivery...");
await Task.Delay(5000);
Console.WriteLine("finish eating");
}
static async Task LearnCsharp()
{
Console.WriteLine("learning C-sharp...");
await Task.Delay(10000);
Console.WriteLine("finish learning");
}
static async Task Main(string[] args)
{
var p= Stopwatch.StartNew();
OrderDelivery();
var waitingdelivery = WaitDelivery();
var learingCsharp = LearnCsharp();
await waitingdelivery;
await EatDelivery();
await learingCsharp;
//这里注意 await waitingdelivery与await WaitDelivery()的区别
//await WaitDelivery()会重新启动一个新的方法,并等待其结束,会发生阻塞
//而await waitingdelivery
//只是等待原来的方法结束,并且会释放这个线程,不会发生阻塞
//WaitDelivery();
//LearnCsharp();
//await WaitDelivery();
//await EatDelivery();
//await LearnCsharp();
p.Stop();
Console.WriteLine(p.Elapsed.TotalSeconds);
}
这样程序的运行时间就减少为了10秒