毕业设计要用到c#。
在学过了c++和java后,初看《c#入门经典》和《c#高级编程》,不免会把c#和java与c++在语法方面做些比较。做下笔记。
预编译:
像C++一样,c#中有预编译命令
namespace 和 using:
namespace和java的package相当,不像java那样一个文件中只能有一个public类,命名空间是逻辑组合,不是物理组合,一个文件可以有多个public类,一个类也可以写在多个文件中。namespace可以嵌套;可以给namespace起别名,如using S = System.
MyClass a = new MyClass(); Console.WriteLine(a.GetType().Namespace);
基本数据类型:
像c++中一样有unsigned类型,如uint,ushort等等(在不安全代码中还可以进行指针操作),还有一个decimal类型。
string(引用类型)可以像数组那样使用:string s = "haha"; char c = s[0];
匿名类型var:
var s = "string";//在编译的时候根据等号右边的类型转换成string类型 var a = new //等号右边是一个匿名类型 { name="hh", age = 3, };
goto:
java中被废掉的goto关键字又回来了
const 和 readonly:
定义常量用const关键字。类中的const修饰的常量默认就是静态的了(相当于java中的static final);而readonly修饰的变量可以不是静态的,但只能在定义时赋值或构造函数中设置(相当于java中的final)。const常量是在编译时设置,而readonly在运行时设置的。
@:
@符号后面的字符串就是其字面意思(没有转意字符),比如string s = @"\n" 则s的内容就是"\n"而不是一个换行符
foreach:
int[] intArray = new int[3]; foreach(int i in intArray){...}
switch:
可以用于string;default不一定要写在最后一句,但效果和写在最后一样;每个case必须有break或return或goto case ...结束,两个或多个case连续写在一起的情况除外
enum:
默认的underlying type是int,从0开始赋值,可以像int一样进行运算,也可以和int相互转换。也可以underlying type也可以是其他的基本数据类型,也可以自己赋值。
enum Orientation:byte{//指定underlying type为byte east, west=3, north, south, north, unknown=east } ... Orientation o = Orientation.south; byte i = (byte)o; Orientation east = (Orientation)0; Orientation direction = (Orientation)Enum.Parse(typeof(Orientation), "north");//字符串转化为枚举
值类型struct:
struct派生于System.ValueType,它又派生于System.Object
struct和class很像,同样可以有属性和方法,也可以实现接口,但不能继承。重要的是c#中struct是值类型,而class是引用类型 。(c++中struct翻译为结构体,但是c++中的struct和class都是值类型)
MyStruct struct1 = new MyStruct(); MyStruct struct2 = struct1; //struct1复制了一份给struct2,对struct2的设置不会影响struct1 MyClass class1 = new MyClass(); MyClass class2 = class1; //对class2的修改会影响class1,因为它们指向同一个实例
typeof运算符:
typeof(ClassName)感觉像java里面的Object.class,而object.GetType()像java里面的object.getClass();
可用(Nullable)修饰符?
int? a = null;//这样值类型和枚举类型就可以被赋值为null了 Console.WriteLine(typeof(int?));//int? 是有别于int 的另一种类型 a = 3; Console.WriteLine(a.GetType());//奇怪的是被赋值之后a又变回int类型了
ref,out:
ref使得参数按引用传递,学过c++的话对这个不会陌生
void Triple(ref int a) {a *= 3;} ... int a = 3; Triple(ref a); Console.WriteLine(a);//a变成9
out和ref功能和用法相同,区别在于用ref传入函数的参数必须先初始化,out不用
params:
params关键字使函数可以接受个数任意的参数
int fun(params int[] a){...}就像java中的int fun(int...a){...}
Main函数:
有4种static int Main(string[]args),static void Main(string[]args),static int Main(),static void Main()。Main函数的可见性无所谓
多维数组,变长数组:
int[][] array = new int[3][];// 变长数组 array[0] = new int[3]; array[0][1] = 43; int[,] array2 = new int[4, 5];//多维数组 array2[0, 1] = 32;
而java中都是变长数组,即数组的数组
checked和unchecked:
使用checked会在溢出是抛出异常,默认是unchecked
byte b = 255; checked { b++; }//抛出异常 Console.WriteLine(b);
delegate:
委托(代理)好像就是c++中的函数指针,委托也是一种类,派生于System.MulticastDelegate,而System.MulticastDelegate有派生于System.Delegate,MutlicastDelegate是Delegate的链表。
int add(int a,int b){return a+b;} int subtract(int a,int b){return a-b;} delegate int Fun(int a,int b); Fun f = add; int a = f(1,1);//得到2 f = subtract; int b = f(1,1);//得到0 Fun g = f + add; Fun g2 = g; g2 -= add;//值得注意的是delegate虽是应用类型,但这里对g2的修改并不会影响g,这个和string类型一样的道理
多播委托可以使用+、-、+=、-=运算符
event:
事件处理时同样可以用委托delegate来处理,但加了event关键字后编译器会对委托进一步封装,限制了一些事件发布时逻辑上可能出现的错误
我原来以为事件在概念上很难OOP思想来理解,不如java中监听器的概念清晰;但看了《c#本质论》知道了事件和委托,还有属性被编译器编译后也是用类的方法来实现的
集合:
Array和ArrayList等都用中括号[]来索引,Dictionary还可以用任意类型的引索(这让我想到了js)
Dictionary<string, int> dic = new Dictionary<string, int>(); List<int> l = new List<int>(); ... int a = (int)al[0]; int b = dic["haha"]; //如果[key]找不到对应的value会抛出异常而不是返回null,有一个TryGetValue方法不会抛异常
函数重载:
virtual显式指定虚函数或虚属性。不用virtual则默认不是虚拟的(这和C++一样,而java则默认都是虚拟的)。
override关键字用于重载虚函数,new关键字用于显式声明子类的函数隐藏了父类的同名函数。
class A { public void M() { Console.WriteLine("a"); } public virtual void V() { Console.WriteLine("a"); } } class B : A { public new void M() { Console.WriteLine("b"); } public override void V() { Console.WriteLine("b"); } } ... A a = new B(); B b = new B(); a.M();//a a.V();//b b.M();//b b.V();//b
虚函数的调用取决于实例的类型,而非虚函数的调用取决于引用变量的类型
试了一下发现verride不能和virtual一起用,但如果将来有一个C类要重载B类的V()方法,怎么使B.V()成为虚函数啊?
操作符重载:
重载的方法和C++中的一样(记得学C++时我最喜欢这个了,很方便啊),但限制比C++多。比如必须声明为public和static;不能重载“+=”,重载“+”之后“+=”自然可以使用;不能重载“=”;可以重载“&”,不能重载“&&”;“<”和“>”、“<=”和“>=”、“!=”和“=”要成对重载,不能只重载一个;而且好像只能重载成静态static的。
class Time { public int hour, min; public Time(int h, int m) { hour = h; min = m; } public static Time operator +(Time a, Time b) { return new Time(a.hour + b.hour, a.min + b.min); } }
自定义数据类型转换:
很强大啊,用法像重载操作符一样,同样必须是public static。可以在类和基本数据类型之间,类和类之间转换。implicit指明可以隐式转换,explicit指明必须显示转换。转换函数必须写在源类型或目标类型的类或结构体定义里面,有继承关系的本来就可以转换的类之间不能重载。
class C { public int x; public C(int i) { x = i; } public static implicit operator int(C c) { return c.x; } public static explicit operator C(int i) { return new C(i); } } ... int i = new C(3);//i=3 C c = (C)5;//c.x=5 byte b = (byte)new C(2);//b=2,C被转换成int类型后被转换成byte
boxing和unboxing:
在引用类型和值类型之间相互装换,boxing是自动进行的,而unboxing是要显式说明的
int i = 0; object o = i;//i被自动装箱成引用类型o string str = 2.ToString();//2被被自动装箱成一个临时的引用类型 int b = (int)o;//取消装箱 Console.WriteLine(Object.ReferenceEquals(i,i)); //值类型用ReferenceEquals(a,b)函数进行比较总是得到false,因为装箱之后的引用变量指向了原来值类型变量的一份拷贝,而不是原值。这样对装箱对象的操作不会影响原来的值类型的内容。
《C#高级编程》上的意思是:定义结构体时,.Net Framework会隐式的提供一个隐式类,与其结构体相同,却是一个引用类型,用于装箱。枚举类型也有类似情况。
is和as:
“is”和java中的“instanceof”相当;as用于尝试类型转换
ClassA a = (ClassB)b;//无法转换时可能抛出异常 ClassA a = b as ClassA//无法转换时a=null
unsafe
c# 中的基本数据类型可以像C语言那样有指针操作,但这种不安全代码要用unsafe{...}括起来。下面的代码用指针将一个int整型分解成4个字节,再将4个字节合成一个int:
int beforeConvert = 5; byte[] convertedBytes = new byte[sizeof(int)]; int afterConvert; unsafe { Console.WriteLine("int " + beforeConvert + " to bytes:"); byte* p = (byte*)&beforeConvert; for (int i = 0; i < sizeof(int); i++) { convertedBytes[i] = *p++; Console.WriteLine("bytes[" + i + "]=" + convertedBytes[i]); } p = (byte*)&afterConvert; for (int i = 0; i < sizeof(int); i++) { *p++ = convertedBytes[i]; } Console.WriteLine("bytes to int: "+afterConvert); }
lock:
和java中的"synchronized”相当,感觉还是"lock”的意思比较直白。
lock(object){...}只是一种简便的语法,实际编译后还是用一个Monitor类的Enter()和Exit()方法实现的同步
lock语句不能用值类型变量当做锁,因为值类型会被封箱成引用类型,而引用指向了新的地址,已不是同一个变量,所以不能实现同步。关于boxing和unboxing《c#本质论》讲的比较清楚
关于类的可见性:
protected——只能由类或派生类的访问
internal——只能由定义它的工程(程序集)内部代码访问,可修饰类,类不写public默认就是internal
(java中的protected 成员相当于c#中的protected internal 成员)
private——默认不写就是private
sealed——相当于java中的final,使类不能被派生,或使函数不能被重载
extern——不很清楚,引入dll中的外部函数时有用
static——修饰静态成员;也可以修饰类,一个静态类是只有静态成员的类,不能实例化,比如Math类
property:
使用时让初学者分不清是属性(property)还是字段(field),其实和java中的setter/getter方法一样的功能,还弄了一大堆花括号,真是的...
不过慢慢就会发现属性的方便之处,可以直接像字段那样使用。
接口中不但可以包含方法,也可以包含属性,指定实现接口的类必须提供的字段
PascalCasing:
C#习惯定义变量名的时候头字母大写,有时我都分不清某个函数是静态的还是非静态的,因为调用它的前面那个名字不知是类名还是变量名。感觉还是camelCasing比较可爱
继承:
同样是单继承、多接口,继承的类写在第一个,然后是要实现的接口,用逗号隔开。父类用base关键字而不是super,构造函数后面可用冒号跟其他的构造函数
interface I { void DoSth();} class A { private int a; public A(int a) { this.a = a; } public void Method() { Console.WriteLine("A.Method()"); } } class B : A ,I { private int b; public void DoSth(){} public new void Method() { base.Method(); Console.WriteLine("B.Method"); } public B(int a, int b) : base(a) { this.b = b; } public B(int b):this(0,b){} }
跨线程调用窗体控件:
这在java中是允许的,尽管这么做不是线程安全的;但是在c#中默认是会出异常的。比如:
private void button1_Click(object sender, EventArgs e) { Thread t= new Thread(() => { int i = 0; while (i++ < 10) { textBox1.AppendText("i");//抛出异常,因为textBox1是在UI线程中生成的,默认不能在其他线程中被调用 Thread.Sleep(1000); } }); t.IsBackground = true; t.Start(); }
解决办法一种是让程序允许扩线程调用控件,在窗体的构造函数中加入一句允许跨线程,但这样做仍然不是线程安全的:
public Form1() { InitializeComponent(); Form.CheckForIllegalCrossThreadCalls = false;//加入这句 }
另一种方法比较正统:
private void button1_Click(object sender, EventArgs e) { Thread t= new Thread(() => { int i=0; while (i++ < 10) { Append(); Thread.Sleep(1000); } }); t.IsBackground = true; t.Start(); } void Append() { if (textBox1.InvokeRequired) { textBox1.Invoke(new Action(Append)); } else { textBox1.AppendText("i"); } }
异常处理:
java强制要处理异常或抛给调用函数,而C#中抛出异常的函数不必非要用try/catch 处理也不会报错(是不是这样可以提高性能?),但使得初学者不知道某个函数可能抛出什么异常,也弄不清什么情况下要处理异常。
在捕获异常,做一些适当处理后可以用“throw;”语句再次抛出异常;这与“throw new Exception(...)”重新引发异常的区别是:后者将栈追踪重置为重新引发的位置,而前置栈追踪位置是原始引发位置