前言
我此前有2年+的python工作经验(游戏行业),后来技术栈换成了unity,用的C#+lua。lua是比较好上手的,毕竟也是脚本。不过C#自己还是不太熟悉,虽然C++都是学过,C#上学的时候也学过,哈哈都忘了,还是想花点时间熟悉一下C#,增加一点自己看C#代码的自信。
所以,如果你是一个脚本语言比较熟悉的程序,本文十分适合你,只要你是一个熟练老手,或者是已经掌握了那么1、2种编程语言,看这篇文章熟悉C#再适合不过了。不过即使你是一个刚刚开始学习编程语言的小白,本文展示的也是那些C#中比较特别或者其他语言也有,但是C#中叫法不同的一些点。(如果本文看不懂,可能需要去补C++基础。) c#参考: 菜鸟教程(本文也可以当作菜鸟文档的辅助性文档来看) 微软官方文档
基础篇
说的一门高级语言的基础,也就是变量、运算符、结构体、函数、类那些事。需要强调的如下:
变量命名
和C++类似
- 可以用 下划线_ 以及 字母 开头命名变量
- 大小写敏感
- 可以包含_,字母,数字
- 不能用保留字
但是C#可以使用@开头,并且命名里其他位置也可以有@。
字符串常量 @“”
c# 中字符串之前加@ 可以使得字符串按照原格式,并且无需对特殊字符转义。 比如@“a\bbb”, 打印就是“a\bbb”,不用在\前面加转义。
可空类型(Nullable)
C#里有一个比较有意思的可空类型。 就是一般情况下,值类型是不允许被赋值为null的,比如int,bool等,不过c#允许显示声明这些值类型是可以被赋空值的,显示声明的语法如下:
int? i;
i = null;
c#还提供了一个Null 合并运算符 有点类似与pytho以及lua的 or,比如a = b or "haha" 就表示如果b为空,a就被赋值为"haha" ,如果b不为空,a就被赋值为b的值。 c#的类似语法写法如下:
double? num1 = null;
double num2;
num2 = num1 ?? 5.34;
这里,如果num1 为null,那么num2就为5.34。(此处确实是5.34)。 这里需要注意的是:??前面的变量必须是可空类型的。(例子里就是num1必须是可空类型)否则编译报错。
数组
初始化数组的方式:
int[] balance = new int[10];
int[] balance = {1,2,3};
int[] balance = new int[3]{1,2,3};
c#里多维数组不同于c++,可以认为c++里的多维数组等同于数组的数组,而c#对两者进行了区分,c#中数组的数组被称为交错数组。多维数组以及交错数组的的声明、初始化,使用如下:
int [3,4] names;
int [,] names = new int [3,4]{
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}};
int val = names[2,3]
int [][] scores;
int[][] scores = new int[2][]{new int[]{92,93,94},new int[]{85,66,87,88}};
int val = scores[1][1]
参数数组
用params修饰 表示未知数量的参数,类似python的**kwargs, 传参时候可以传数组实参,也可以传一组数组元素。 代码如下:
public int AddElements(params int[] arr){
int sum = 0;
foreach (int i in arr){
sum += i;
}
return sum;
}
int []p = new int []{1, 2, 3, 4, 5};
int sum = app.AddElements(p);
int sum = app.AddElements(1, 2, 3, 4, 5);
数组的基类 Array
详见C# Array 类 常用的属性如 Length,方法如Sort()等
int []add = new int[]{3,4,5};
if (add is Array){
Console.WriteLine("true");
}
结构体
循环
foreach 类似于python for a in list 或者lua中 for i,v in ipairs(table)
int []n = new int[10];
foreach (int j in n )
{
}
结构 struct
和c++一样,c#的struct也是值类型。简单来说,可以认为是轻量的类(类是引用类型),不支持继承、不支持重写默认构造函数。具体参考这里
枚举
enum Days { Sun, Mon, tue, Wed, thu, Fri, Sat };
Days a = Days.Sun;
函数传参方式修饰符
C#里有 ref 以及 out这两个传参方式修饰符 C#中有三种参数传递方式:
- 值传参:实参和形参有不同的内存空间,函数内对参数的值进行更改,不会影响函数外实参的值。
- 引用传参:实参和形参有相同的内存空间,改形参的值等于改实参的值,实参需要初始化。ref修饰。
- 输出传参:相当于引用传递,但是实参可以不初始化,仅声明即可。out修饰。
ref的使用如下段代码所示:
void fun(ref int param){
param = 100;
}
int a = 10;
fun(ref a);
访问修饰符
默认是private, 除了private,还有public、protected、internal、protected internal一共5种访问修饰符。 引用网友的总结 比如说:一个人A为父类,他的儿子B,妻子C,私生子D(注:D不在他家里) 如果我们给A的事情增加修饰符: public事件,地球人都知道,全公开 protected事件,A,B,D知道(A和他的所有儿子知道,妻子C不知道) private事件,只有A知道(隐私?心事?) internal事件,A,B,C知道(A家里人都知道,私生子D不知道) protected internal事件,A,B,C,D都知道,其它人不知道
运算符重载
类似c++,这里简单举个代码的例子。例子参考菜鸟教程。主要是opreator关键字,有些运算符可以重载,有些不可以。
public static Box operator+ (Box b, Box c){
Box box = new Box();
box.length = b.length + c.length;
box.breadth = b.breadth + c.breadth;
box.height = b.height + c.height;
return box;
}
命名空间
namespace以及using这两个关键字。参考菜鸟教程
类
类的默认访问标识是internal,成员的默认访问标识是private。类基本和c++类似,都有构造函数、析构函数、成员初始化列表等,不过c#不支持多继承,只支持单继承,c++支持多继承,不过c#可以用接口来实现和多重继承等同的效果。 下面代码涵盖了c#类相关的一些基本内容,注释给出了进一步的描述。
基础
using System;
class Animal{
static public int count = 0;
void countAdd(){
count += 1;
}
protected Animal(){
countAdd();
Console.WriteLine("Create Animal!");
}
protected Animal(string name){
countAdd();
Console.WriteLine("Create Animal with name {0}!", name);
}
~Animal(){
Console.WriteLine("Delete Animal!");
}
}
interface Friend{
public void playWithPeople();
}
class Cat:Animal{
public Cat(){
Console.WriteLine("Create Cat!");
}
public Cat(string name):base(name){
Console.WriteLine("Create Cat with name {0}!", name);
}
~Cat(){
Console.WriteLine("Delete Cat!");
}
}
class Dog:Animal,Friend{
public Dog(){
Console.WriteLine("Create Dog!");
}
public Dog(string name):this(){
Console.WriteLine("Create Dog with name {0}!", name);
}
public void playWithPeople(){
Console.WriteLine("This dog is playing with people!");
}
}
class HelloWorld{
static void Main(string[] args){
Cat cat = new Cat("catty");
Dog dog = new Dog("doggy");
Console.WriteLine(Animal.count);
Console.WriteLine(Cat.count);
Console.WriteLine(Dog.count);
dog.playWithPeople();
}
}
运行程序最后输出的是
Create Animal with name catty!
Create Cat with name catty!
Create Animal!
Create Dog!
Create Dog with name doggy!
2
2
2
This dog is playing with people!
多态
和c++类似 抽象类可以定义抽象方法,用abstract修饰抽象类,涉及多态以及方法重写的几个关键字:abstract、vritual、new、override
- abstract可以修饰类,可以修饰方法。抽象方法必须声明在抽象类中,抽象方法不能有实现,必须被子类实现,子类方法之前必须有override。
- vritual 修饰方法,抽象类和虚方法就可以实现多态,虚方法可以有实现,也可以被子类实现。
- new 覆盖基类方法, 子类(与父类)同名方法 默认的修饰,可以隐藏父类的abstract or vritual方法,也可以是普通方法,甚至没有声明的方法(不过对于没有声明的方法不会跑错,会有warning)。不实现多态,声明的什么类型就调用什么类型的方法。
- override 重写,重写父类的方法,父类的方法可以是抽象方法或者虚方法(只能是二者之一),重写会实现多态:即调用实例真正类型的方法。
关于 new和override的区别 例子如下
Animal catA = new Cat();
catA.speak();
catA.run();
比如Cat类继承Animal类,Animal定义两个虚方法,speak以及run,Cat重写了这两个方法,用override修饰speak,用new修饰run,上面的代码 speak调用的是Cat类的实现,而run调用的是Animal的实现。
预处理
菜鸟教程里表示c#里好像没有宏定义,c++里是有的,这里我先不深究,不过c#里也有类似宏定义的功能,就是#define,可以直接看菜鸟教程,之后我可能会补充。 关于宏定义的利弊,可以参考知乎上 皮皮关的回答
异常处理
一场处理常见的那几种语言都差不多,c#和java一样,比c++多一个关键字 finally,另外三个关键字就是try,catch, throw。
- try 后面跟一段可能抛出异常的代码
- catch后面跟异常处理代码
- finally后面跟不管有没有异常都要执行的代码,比如打开文件之后,不论有没有异常 文件都要关闭
- throw用于主动抛出异常
可以自定义异常,继承 System.ApplicationException 即可。
文件读写
直接看菜鸟教程:文件的输入与输出吧,哈哈。
高级篇
属性(properties)
参考官方文档 c#中的属性特指通过get or set 访问器(accessors)读写的数据域(field),有点类似于python的@property以及@name.setter(python里不定义setter就是一个只读属性),python声明访问器的方式比较简单,就是一个装饰器@property(等于c#中的get),c#定义访问器的语法会稍微显得正式(复杂)一些,如下:
class Student{
private string code = "sq";
public string Code{
get{
return code;
}
set{
code = value;
}
}
}
Student s = new Student();
s.Code = "hahaha";
Console.WriteLine(s.Code);
另外,C#的访问器可以使用简化版,其中= "instial string" 是初始化语法,非必须,按需要使用,get,set至少有其中一个即可;
public string Code{get;set;} = "instial string";
当 get以及set的主体只有一个表达式的时候,还可以简化如下(expression-bodied members ):
class Student{
private string code = "sq";
public string Code{
get => code;
set => code = value;
}
}
反射
菜鸟教程对于反射的定义是:
反射指程序可以访问、检测和修改它本身状态或行为的一种能力。
所以反射是一种能力,或者说一种特性,不同语言实现这种特性的方式会不太一样,比如: python这种解释型语言,本身就可以动态(运行时)给对象添加属性(修改了对象本身的状态),或者用getAttr、setAttr运行时访问、修改自身的属性。 Java是在运行时,由JVM(Java virtual machine,Java虚拟机)加载一个Class类型的对象(class Class),这个对象会对应某个类型的对象,比如有一个自定义的People类,运行时加载这个类的时候,JVM就会创建一个Class对象cls(其实我想称他为元对象,meta object,描述类的对象),这个对象提供一些方法,比如cls.getName(),获取类名,或者获得某个属性的值,cls.getField(fieldName),具体可以参考廖雪峰的官方网站 C# 和Java一样,都是编译型的语言(虽然不是类似C语言那样是纯粹的编译语言,就是编译之后可以直接在计算机执行,Java编译之后还需要JVM解释执行,具体可以参考这里,一般更偏向把C#,Java称为编译型的语言。)其实C#实现反射的方式也类似,C#内置Assembly、Type、MethodInfo等类型,可以运行时访问对象自身的一些属性等。参考官方文档:Reflection (C#),官方文档:Reflection in .NET
int i = 42;
Type type = i.GetType();
Console.WriteLine(type);
特性
参考官方文档 特性简单来说可以认为是提供一种为代码实体,比如类、方法等添加声明性信息,或者说是元数据的方式。元数据 就是描述数据的数据,比如一个类People,有一个属性是身高,身高的单位是cm还是m,就是描述身高这个数据的数据,就是元数据。我的理解,特性的本质就是元数据。 C#中为对象添加特性,或者说添加元数据的方式就是通过一种类似标签的语法:
[attribute(positional_parameters, name_parameter = value, ...)]
比如.Net框架或者说c#有一个内置标签是Obsolete,表示被标记的程序元素是废弃的,过时的。比如他可以标记在类前面:
[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass{
}
以上主要是展示一下特性的语法以及怎么用。 还需要知道的是c#里的预定义特性,以及如何自定义特性、并且使用自定义特性。这里有一个事实要先说明一下,就是c#里,所有的特性,本质是一个类,所有的特性,不论是预定义还是自定义,都继承System.Attribute这个类。
预定义特性
AttributeUsage
这个特性简单来说是特性的特性,具体来说,这个特性定义一个特性可以应用到那些代码元素上。 允许添加特性的代码元素有很多,下面列举了可能被添加特性的代码元素。
- Assembly
- Class
- Constructor
- Delegate
- Enum
- Event
- Field
- GenericParameter
- Interface
- Method
- Module
- Parameter
- Property
- ReturnValue
- Struct
比如,我要自定义一个特性(就是继承System.Attribute这个类,后面会讲)。我自己写了一个特性类,叫MyOwnAttribute,下面的写法,就是给我的自定义特性类加了AttributeUsage这个特性,声明MyOwnAttribute这个特性只可以用在类以及结构前面。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyOwnAttribute: Attribute{
}
如果把这个MyOwnAttribute定义在方法前面,会有编译报错。
自定义特性
预定义特性的用法,c#会处理,所以当我们给代码元素加上一些预定义特性的时候,程序会有一定的行为,比如AttributeUsage会让编译的时候,对特性进行检查,看是否有特性声明在了不允许使用的代码元素前面。我们也知道,特性作用的本质,只不过是给目标添加一些元数据而已,自定义特性的本质就是为目标,或者程序元素添加一些自定义元数据,这些元数据本身不会引起一些运行时的行为,一般情况下,程序员添加自定义元数据的目的往往是希望程序对标记了特性的代码有特定的处理。那这个往程序对标记了特性的代码有特定的处理本身也要求语言具备反射能力。 所以,总的来说,自定义特性本质是自定义元数据,而依托这些自定义元数据产生的不同的代码行为本身又依赖c#的反射能力,即自定义特性一般配合反射使用。 说了这么多,咱们举个例子。这个例子包含两步:
- 自定义特性
- 使用自定义特性,即根据特性来对代码做不同的处理
比如我想定义一个叫WillPrintName的特性,希望这个特性可以标记在类或者方法上,当这个类被初始化或者方法被调用的时候,我的代码会自己打印出实例化的类名或者调用的方法名。
自定义特性的定义
定义代码如下,具体可以看看注释。 注意:WillPrintNameAttribute一般以Attribute作为命名结尾,当然不强制,只不过已Attribute结尾,使用的时候,可以省略Attribute,直接使用[WillPrintName],算是c#的一种约定俗成。
using System;
namespace HelloAttribute{
[WillPrintName]
class People{
public People(){
Console.WriteLine("Create People!");
}
[WillPrintName]
public void smile(){
Console.WriteLine("This man is smiling!");
}
}
[AttributeUsage(AttributeTargets.Class |AttributeTargets.Method)]
class WillPrintNameAttribute:Attribute{
public WillPrintNameAttribute(){
Console.WriteLine("Create WillPrintNameAttribute!");
}
}
class HelloAttribute{
static void Main(string[] args){
People people = new People();
people.smile();
}
}
}
运行之后可以看到结果:
Create People!
也就是说,特性类本身并没有因为声明而被创建,就是[]并没有创建特性对象,只是给People类添加了标签,也就是元数据而已。那如何让被特性标记的类 实例化的时候自报类名,就要用到反射了,c#的反射特性,允许可以通过写代码运行时拿到声明的特性,怎么做,需要两步:
- 拿到代码元素对应的元对象,比如class 对应的元对象就是TypeInfo,method对应的元对象就是MethodInfo
- 调用元对象的 GetCustomAttributes方法,就可以获得这个代码元素上声明的特性
还是之前那个例子,代码改动如下:
using System;
using System.Reflection;
namespace HelloAttribute{
[WillPrintName]
class People{
public People(){
Console.WriteLine("Create People!");
}
[WillPrintName]
public void smile(){
Console.WriteLine("This man is smiling!");
}
}
[AttributeUsage(AttributeTargets.Class |AttributeTargets.Method)]
class WillPrintNameAttribute:Attribute{
public WillPrintNameAttribute(){
Console.WriteLine("Create WillPrintNameAttribute!");
}
}
class HelloAttribute{
static Object getObjAndPrintClassName(Type type){
Object obj = System.Activator.CreateInstance(type);
TypeInfo typeInfo = type.GetTypeInfo();
var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
if(attr.GetType().Name == "WillPrintNameAttribute"){
Console.WriteLine("实例化类: " + type.Name);
}
return obj;
}
static void runMethod(Object obj, string methodName,Object ?[] parameters){
MethodInfo method = obj.GetType().GetMethod(methodName);
method.Invoke(obj, parameters);
var memberAttrs = method.GetCustomAttributes();
foreach(var attr in memberAttrs)
if(attr.GetType().Name == "WillPrintNameAttribute"){
Console.WriteLine("调用方法: " + methodName);
}
}
static void Main(string[] args){
People people = (People)getObjAndPrintClassName(typeof(People));
runMethod(people,"smile",null);
}
}
}
运行结果如下:
Create People! Create WillPrintNameAttribute! 实例化类: People This man is smiling! Create WillPrintNameAttribute! 调用方法: smile
我们可以发现WillPrintNameAttribute创建了两次,事实上,method.GetCustomAttributes() 的调用会触发特性的实例化,每次调用都会触发。
特性总结
- 特性的本质是给类、方法等代码元素添加元数据;
- c#中的自定义特性是自己实现的Attribute的子类;
- 特性的声明通过[]语法,其声明不会实例化特性对象;
- 特性和代码(行为)的绑定需要依托于反射,c#通过创建元对象实现反射,元对象的GetCustomAttributes方法的调用会实例化元对象对应的代码元素上特性的实例化。
可能有点绕,可以仔细品一品,元对象就是指TypeInfo、MethodInfo等对象
==================================================== 分割线,下面待补充
索引器
委托
事件
集合
泛型
匿名方法
不安全代码
多线程
|