一、C#基础
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 WriteLine("Hello World!"); 14 } 15 } 16 }
名称空间是把相关类组合在一起的方式,namespace关键字声明了与类相关的名称空间。其后花括号中的所有代码都被认为是在这个名称空间中。编译器在using语句指定的名称空间中查找没有在当前名称空间中定义但在代码中引用的类。
using static System.Console;语句允许引用这个类的静态成员,而忽略名称空间和类名。
在源代码中,声明了一个类Program。但是该类位于名称空间HelloWorldApp中,所以其完整的名称是HelloWorldApp.Program。
所有的C#代码都必须包含在类中。类的声明包含class关键字,其后是类名和一对花括号。与类相关的所有代码都应放在这对花括号中。
Program类包含一个方法Main()。每个C#可执行文件都必须有一个入口点——Main()方法。在程序启动时调用该方法。该方法要么没有返回值(void),要么返回一个整数(int)。
二、变量
在C#中声明变量使用下述语法:
datatype identifier;
1、初始化变量
C#编译器需要用某个初始值对变量进行初始化,之后才能在操作种引用该变量。大多数现代编译器把没有初始化标记为警告,但C#编译器把它当成错误来看。
C#有两个方法可确保变量在使用前进行了初始化:
- 变量是类或结构中的字段,如果没有显式初始化,则创建这些变量时,其默认值就是0。
- 方法的局部变量必须在代码中显式初始化,之后才能在语句中使用它们的值。
2、类型推断
类型推断使用var关键字。声明变量的语法有些变化:使用var关键字替代实际的类型。编译器可以根据变量的初始化值“推断”变量的类型。
使用类型推断需要遵循一些规则:
- 变量必须初始化。否则,编译器就没有推断变量类型的依据。
- 初始化器不能为空。
- 初始化器必须放在表达式中。
- 不能把初始化器设置为一个对象,除非在初始化器中创建了一个新对象。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 var name = "Hello World!"; 14 var age = 18; 15 var ok = true; 16 var name_type = name.GetType(); 17 var age_type = age.GetType(); 18 var ok_type = ok.GetType(); 19 WriteLine($"name type: {name_type}"); 20 WriteLine($"age type: {age_type}"); 21 WriteLine($"ok type: {ok_type}"); 22 } 23 } 24 }
3、变量的作用域
变量的作用域是可以访问该变量的代码区域。一般情况下,确定作用域遵循以下规则:
- 只要类在某个作用域内,其字段(也称成员变量)也在该作用域内。
- 局部变量存在于表示声明该变量的块语句或方法结束的右花括号之前的作用域内。
- 在for、while或类似语句中声明的局部变量存在于该循环体内。
1)局部变量的作用域冲突
大型程序在不同部分为不同的变量使用相同的变量名很常见。只要变量的作用域是程序的不同部分,就不会有问题,也不会产生多义性。但要注意,同名的局部变量不能在同一作用域内声明两次。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 for(int i = 0; i < 10; i++) 14 Write(i); 15 WriteLine(); 16 for (int i = 9; i >= 0; i--) 17 Write(i); 18 WriteLine(); 19 } 20 } 21 }
在同一个方法中i声明了两次。可以这么做的原因是i在两个相互独立的循环内部声明,所以每个变量i对于各自的循环来说是局部变量。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 int j = 1; 14 for(int i = 0; i < 10; i++) 15 { 16 int j = 2; 17 WriteLine(j + i); 18 } 19 } 20 } 21 }
编译出错。原因:变量j是for循环开始前定义的,直到Main()方法执行结束,变量j才超出作用域。第二个j(不合法)虽然在循环的作用域内,但作用域嵌套在Main()方法的作用域内。因此,编译器无法区分这两个变量,所以不允许声明第二个变量。
2)字段和局部变量的冲突
某些情况下,可以区分名称相同(尽管其完全限定名不同)、作用域相同的两个标识符。此时编译器允许声明第二个变量。原因是C#在变量之间有一个基本的区分,它把在类级别声明的变量看成字段,而把在方法中声明的变量看成局部变量。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static int j = 20; 12 static void Main(string[] args) 13 { 14 int j = 30; 15 WriteLine(j); 16 } 17 } 18 }
一个是在类级别上定义的j,在类Program删除前是不会超出作用域的;一个是在Main()中定义的j。这里,Main()方法中声明的新变量j隐藏了同名的类级别变量,所以在运行这段代码时显示的是30。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static int j = 20; 12 static void Main(string[] args) 13 { 14 int j = 30; 15 WriteLine(Program.j); 16 } 17 } 18 }
使用语法object.fieldname,在对象的外部引用类或结构的字段。在上述代码中,访问静态方法中的一个静态字段,所以只能使用类本身的名称。如果要访问实例字段(该字段属于类的一个特定实例),就需要使用this关键字。
4、常量
常量是其值在使用过程(生命周期)中不会发生变化的变量。在声明和初始化变量时,在变量的前面加上关键字const,就可以把该变量指定为一个常量。
const int a = 100;
常量具有如下特点:
- 常量必须在声明时初始化。指定了其值后,就不能在改写了。
- 常量的值必须能在编译时用于计算。因此,不能用从变量中提取的值来初始化常量。如果需要这么做,应使用只读字段。
- 常量总是隐式静态的。但注意,不必(实际上,也不允许)在常量声明中包含修饰符static。
三、预定义数据类型
1、值类型和引用类型
C#把数据类型分为两种:值类型和引用类型。
从概念上看,其区别是值类型直接存储其值,而引用类型存储对值的引用。
这两种类型存储在内存的不同地方:值类型存储在堆栈中,而引用类型存储在托管堆上。
如果变量是一个引用,就可以把其值设为null,表示它不引用任何对象。如果将引用设置为null,显然就不能对它调用任何非静态的成员函数或字段,这么做会在运行期间抛出一个异常。
在C#中,基本数据类型都是值类型。
相反,大多数更复杂的C#数据类型,包括我们自己声明的类,都是引用类型。它们分配在堆中,其生存期可以跨多个函数调用,可以通过一个或几个别名来访问。CLR实现一种精细的算法,来跟踪哪些引用变量还是可以访问的,哪些引用变量已经不能访问了。CLR会定期删除不能访问的对象,把它们占用的内存返回给操作系统。这是通过垃圾回收器实现的。
把基本类型规定为值类型,而把包含许多字段的较大类型规定为引用类型,C#设计这种方式是为了得到最佳性能。如果要把自己的类型定义为值类型,就应把它声明为一个结构。
2、.NET类型
数据类型的C#关键字从编译器映射到.NET数据类型。这表示在语法上,可以把所有的基本数据类型看成支持某些方法的类。
3、预定义的值类型
内置的.NET值类型表示基本类型,如整型和浮点类型、字符类型和布尔类型。
1)整型
在C#中,所有的数据类型都以平台无关的方式定义,以备将来把C#和.NET迁移到其他平台上去。
如果对一个int、uint、long还是ulong类型的整数没有任何显式的声明,则该变量默认为int类型。为了把输入的值指定为其他整数类型,可以在数字后面加上字符U、L,也可以使用小写字母。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 uint ui = 1234U; 14 long l = 1234L; 15 ulong ul = 1234UL; 16 } 17 } 18 }
2)浮点类型
如果在代码中对某个非整数值硬编码,则编译器一般假设该变量是double。如果想指定该值为float,可以在其后加上字符F(或f)。
3)decimal
注意,decimal类型不是基本类型,所以在计算时使用该类型会有性能损失。
要把数字指定为decimal类型而不是double、float或整数类型,可以在数字的后面加上字符M(或m)。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 decimal d = 123.4M; 14 WriteLine(d); 15 } 16 } 17 }
4)bool类型
bool值和整数值不能相互隐式转换。
5)字符类型
char类型的字面量是用单引号括起来的,如'A'。如果把字符放在双引号中,编译器会把它看成字符串,从而产生错误。
转义序列:
4、预定义的引用类型
C#支持两种预定义的引用类型:object和string。
1)object类型
许多编程语言和类层次结构都提供了根类型,层次结构中的其他对象都从它派生而来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内置类型和用户定义的类型都从它派生而来。object类型可以用于两个目的:
- 可以使用object引用来绑定任何特定子类型的对象。
- object类型实现了许多一般用途的基本方法。
2)string类型
string是一个引用类型,string对象被分配在堆上,而不是栈上。因此,当把一个字符串变量赋予另一个字符串时,会得到对内存中同一个字符串的两个引用。但是,string与引用类型的常见行为有一些区别。例如,字符串是不可改变的。修改其中一个字符串,就会创建一个新的string对象,而另一个字符串不会发生任何变化。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 string s1 = "hello"; 14 string s2 = s1; 15 WriteLine("s1 is:" + s1); 16 WriteLine("s2 is:" + s2); 17 s1 = "another"; 18 WriteLine("s1 is now:" + s1); 19 WriteLine("s2 is now:" + s2); 20 } 21 } 22 }
字符串字面量放在双引号中;如果试图把字符串放在单引号中,编译器就会把它当成char类型,从而抛出错误。C#字符串和char一样,可以包含Unicode和十六进制数转义序列。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 string s1 = "\\hello"; 14 WriteLine(s1); 15 } 16 } 17 }
可以在字符串字面量的前面加上字符@,在这个字符后的所有字符都看成其原来的含义——它们不会解释为转义字符。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 string s1 = @"\\hello"; 14 WriteLine(s1); 15 } 16 } 17 }
C#6定义了一种新的字符串插值格式,用$前缀标记。对字符串加上$前缀,就允许把花括号放在包含一个变量或代码表达式的字符串中。变量或代码表达式的结果放在字符串中花括号所在的位置。
1 using static System.Console; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace HelloWorldApp 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 string s1 = "hello"; 14 WriteLine($"s1 is {s1}"); 15 } 16 } 17 }