经过siki学院C#前三季教程的笔记整理,鉴于本人先学习java后学习C#的经历,本文将时不时对二者进行比较
1.零碎细节
-
枚举类型本质是一个整数,平时可以通过 (int)emun来获得整数,否则输出的是类
-
结构体是一种抽象数据类型(java没有)
-
如果使用 new 运算符创建结构对象,则会创建该结构对象,并调用适当的构造函数(默认值为0)。与类不同,结构的实例化可以不使用 new 运算符。如果不使用 new,则在初始化所有字段之前,字段都保持未赋值状态且对象不可用。
-
return 直接使用等于中断方法进程,反回调用方法的那一层代码(当然,如果是在main主程里面,那直接整个程序停了),可以在void方法中使用
-
可以把while和try catch联合起来形成检验效果,当使用了try catch,当try里面的一行代码发生异常的时候,会直接不运行下面的内容而直接运行catch,因此while(true){try(输入; break;)catch() } 只要输入有错就不会触发break,如果没错触发了break就解除循环,形成检验效果
-
return和break有一些性质相似的地方(中断和跳出代码的运行),活用可以有特殊效果
-
this代表当前对象,属于方法里面的this.变量实际上代表成员变量(即类里面定义的,所以你方法里面定义和类的成员变量同名的参数也没事,可以用this辨别)
-
基于C#的性质,java和C#的编译器过程有许多的不同,java的编译器是直接使用jvm让高级语言编译为机械语言,C#会先翻译为指令集再交给计算器处理,前者可以直接运行在大部分系统环境中而且速度更快,后者虽然速度慢而且需要下载对应的系统环境,但是可以使用指令平台提供的功能插件,让C#的功能更强大,因此,C#的语法糖远超java,而且可以支持unity的编译。
-
每个方法自带默认构造函数,默认的构造函数会在实例化后,如果没有自定义相关的成员变量,默认的构造函数让类的值变量默认设置为0,让引用变量默认为null,当创建一个新的构造函数的时候,会覆盖掉默认的构造函数(但是默认赋值功能并不会消失,这里存疑),
-
构造函数有类似重构的性质存在:实例化对象的时候,不同的参数会让系统识别,并且动用不同的构造函数
-
属性可以用来自动创建一个成员变量(属性开头最好大写,设置C#编程本身对不大写的行为会有提示)
-
java的bool不可以用0和1来代表,而C#的bool可以,所以C#的bool属于值类型
-
引用数据类型会把引用地址(内存地址)存储在栈内存以指向堆内存的值,值放在堆内存里,但是字符串常量放静态存储区(即使数组会把字符串放堆内存,但是实际上也是引用地址,但是两种理解都可以)
-
构造函数是不能有返回类型的,包括void
-
实例是可以互相赋值的,比如定义了a和b,但是只new了b,然后a=b,那样,b就和a指向一个内存空间。(理清楚赋值的概念,不是赋值引用本身,而是引用背后的内存地址)但是如果都是new(参数);出来的,那样两者使用不同的内存空间,并且互不干扰。(当然,后者占用空间较大)
-
可以把类设置为sealed这样就会被密封,无法被继承,方法也可以,但是必须是被重写的方法才可以(即子类的方法里面)
-
Readonly(只读字段)只能在声明的时候或者构造函数的时候初始化
-
子类重写父类的方法时,不能缩小父类方法的访问权限。只可以放大或者不变。
-
常有人会拿var这个关键字来和dynamic做比较。实际上,var和dynamic完全是两个概念,根本不应该放在一起做比较。var实际上是编译器抛给我们的“语法糖”,一旦被编译,编译器会自动匹配var 变量的实际类型,并用实际类型来替换该变量的声明,这看上去就好像我们在编码的时候是用实际类型进行声明的。而dynamic被编译后,实际是一个object类型,只不过编译器会对dynamic类型进行特殊处理,让它在编译期间不进行任何的类型检查,而是将类型检查放到了运行期。
2.引用与内存地址相关
尽管C#和java在基础语法上有极大的相同性,但是只凭二者编译器过程的不同,就很容易的看的出来,二者在底层构建必定会有许多的不同,而这一切就一定程度的表现在了引用与内存地址这一重要的底层内容。
例如关于已经被声明的对象被null了这件事情,就像上图一样,引用指向的地方变了,但是已经分配给new Object()的内存还是存在与堆内存的,只是没有了引用指向这块内存,所以当c2=c1的时候,c1即使null了,c2依然指向原来的内存,而不是直接也被null了(如下图)(建议把null理解为空引用)
两个类实例可以同时用一个内存空间,这个时候内存自己的计数器就为2,当两者同时null了的时候,计数器变为0,这个时候就成为了“垃圾”,会在合适的时候被回收内存(如下图)
3.值传递相关
java的一切传递都是值传递已经是众所周知,而C#在这一方面体现出来了它相比于java,在语法糖数量上的领先地位——虽然C#不管是值类型或者是引用类型,所有方法参数在++默认情况++下是通过值传递的,但是很明显,它比java多出了引用传递这个选项(尽管后者不是那么常见,而且必须经过特殊声明)
C#比java多出的两个引用传递关键字
out传值
1.定义方法时,参数用out修饰,在调用该方法的时候,参数也需要用out修饰。
2.在定义方法时,必须给参数赋初值。
2.在调用该方法时,可以赋初值,也可以不赋初值。
4.out这个单词是“出去”的意思,所以参数加out就是将方法里面赋的那个值传到外面去了,方法外面赋不赋值都不起作用,所以说out只能将在方法里面赋的初值传出去,但不能将外面赋的值传入。
5.out一般用在函数需要有多个返回值的场所
ref传值
1.ref的用法和out有所不同,相同点是,ref和out一样,方法中的参数用ref修饰,调用此方法的时候参数也需要用ref修饰。
2.不同的是,out只能将值传出,但是ref既可以传入,也可以传出。想要传入,必须在方法外赋值,也就是说,传入方法中的参数必须现在外面被初始化。方法里面可以赋初值,也可以不赋初值。
总的来说,out 是只出不进 ref 又进又出。
经典例题(值传递和引用传递的区别)
Eg:
A a = new A();
a.name = "Name";
setName(a);
public void setName(A obj){
obj.name = "New Name";
obj = new A();
obj.name = "Name";
}
在上面这道题里面,如果a.name的值是New Name,就是值传递,因为参数obj不是变量a,所以变量a还是指向对象A0;如果a.name的值是Name,就是引用传递,因为参数obj就是变量a,这个时候变量a指向的对象被指向到新的对象A1了。而在上面这一题当中,java和C#的答案都是值传递。
4.多态继承相关
- this:指当前类,this调用当前类的属性,方法,包括构造函数的方法,继承本类的构造函数
- base:指当前类的父类,可调用父类的非私有属性,方法,继承父类的构造函数括号里的参数(即java的super)
实际上,base就是儿子继承父类的家产,然后儿子自己还可以有一个同名的this.家产,但是值得一提的是,实际上子类和父类是分开创建的,那不管子类如何用base修改,还是无法对父类造成任何影响
不写base一样调用父类的无参构造函数或者默认构造函数,但是不会继承有参构造函数
继承中对构造函数是不继承的,只是调用
子类在没有自己声明调用父类什么构造函数的情况下,必然会先调用父类的无参构造函数在调用子类自己的构造函数
一旦子类调用了父类的有参构造函数,就不会调用无参的构造函数了
多态
多态有什么好处?
有两个好处:
- 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承
- 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用,
重写覆盖和重载
注意!java的覆盖和重写是一样的,但是C#不同!!!!!
如果不写virtual(虚方法)的话,在派生类定义一个基类的同名方法,当实例化父对象出来后,调用同名方法,并不会调用子类的同名方法,调用的是父类自己的。使用了virtual,再利用override(覆盖)来重写,则父类的虚方法被覆盖了,当实例化父对象出来后,调用的则是子类重写的方法;如果是使用new的话,则只是隐藏了父类方法,当实例化父对象,调用的仍是父类方法,父类方法并没有被完全覆盖(此为重写)。
总结:重写会改变父类方法的功能(override),覆盖不会改变父类方法的功能(new)。
重写的实用例子
用这种重写方式,可以直接改掉你想用的系统功能,改成你想要的方法(但是只有系统允许重写的方法可以这么做)(大部分可以直接调用的方法都是系统方法,而每一个类都会继承系统类Object)
下面这么做,就可以让tostring从输出类路径字符串变成输出里面的函数名字!更符合编程者的需求!
5.静态抽象和接口
静态
Java支持实例访问静态变量,C#不支持,只能类访问静态变量
静态函数只能使用静态数据,静态函数不能访问非静态成员
静态类只能包含静态成员,而且不可以实例化(大部分用来存公共数据)
抽象
抽象类其实是可以被实例化的,但是它的实例化方式并不是通过普通的new方式来创建对象,而是通过父类的应用来指向子类的实例间接地实现父类的实例化,因为子类在实例化之前,一定会先实例化它的父类。这样创建了继承抽象类的子类对象,也就把其父类(抽象类)给实例化了。(所以可以理解为不能实例化)
抽象类好歹允许实现一点功能,接口只能定义方法,而且不能有任何方法体
抽象类和抽象方法诞生的使命就是被继承,被重写。而子类也必须实现抽象父类的全部方法
接口
实现接口时,必须重写所有的方法,(因为接口不可以被实例化,所以接口本身里面只允许static final 类型的常量)
接口不能有修饰符,必须公有,而且不能有字段和构造函数
每个类可以实现多个接口,但是只能继承一个类
接口也可以实现多态,如飞机攻击突然切换为小鸟攻击,如上(类似父类引用子类)
接口可以彼此继承,和类的继承类似(实际使用不多)
6.对于栈内存和堆内存的理解(例题)
struct Student{
public int age;
public string name;
public Student(int age,string name){
this.age=age;
this.name=name;
}
}
public class Test{
static void Mian(){
Student stu1= new Student(18,"小方");
Student stu2= new Student(24,"小刚");
stu2=stu1; //在C#中 结构体是值类型,在这里赋值只会赋值其中变量的值 (相当于给的是一个拷贝的值,而不是内存地址)
stu1.age=30;
stu1.name="小燕";
Console.WriteLine (stu2.age);
Console.WriteLine(st2.name);
}
}
- 此题的答案是18 小方 ,可见stu1并没有像引用变量之间的赋值一样因为stu2后面的改变而改变
- 此题非常经典,重点是结构体和类的存储方式不一样(此题student结构体如果变成类,那结果完全不同!)
- 因为C#的结构体是值类型,所以一定是在栈里面,类在堆里面,导致了结果的不同!(C++有所不同)
- 因为栈空间小,而且速度快,一般适合比较小量的数据
- 堆栈中的数据在内存中地址不确定
总结:请分清楚值传递,值类型,引用传递和引用类型的区别,值类型指的是存在栈里面的变量(无内存地址),引用类型指的是存在堆里面的对象(有内存地址),引用传递指的是直接传入引用对象本身(直接传本体,方法里面改变本体跟着改变,在C#中不用ref基本都是值传递),值传递值得是传入引用对象的拷贝(对拷贝如何修改都不会影响本体)
7.列表类相关
List.insert(index,temp)是插入在index索引的前面!
List.Remove(temp)只会移除匹配到的第一个数据,而RemoveAll会移除所有匹配的数据
List.RemoveAt(index)这个是移除索引的
List.IndexOf(temp)可以查从前到后匹配到的第一个索引,List.LastIndexOf(temp)可以查从后到前匹配的第一个索引,查不到的时候,则会报-1
List.Sort();从小到大排序
8.泛型
泛型适用于经常要改变不同数据的类型的方法和实例的实现
总结:泛型适用于经常要改变不同数据的类型的方法和实例的实现
泛型实战
解决方式
上方就是利用泛型构造了一个可以随时改变运算类型的加法实现类(dynanuc是一个可以实现弱语言的保留字,具体就是可以让程序干脆不检测类型是否有误)