文章目录
一、 基本语法
使用下面这段代码作为例子:
using System;
namespace RectangleApplication
{
class Rectangle
{
// 成员变量
double length;
double width;
public void Acceptdetails()
{
length = 4.5;
width = 3.5;
}
public double GetArea()
{
return length * width;
}
public void Display()
{
Console.WriteLine("Length: {0}", length);
Console.WriteLine("Width: {0}", width);
Console.WriteLine("Area: {0}", GetArea());
}
}
class ExecuteRectangle
{
static void Main(string[] args)
{
Rectangle r = new Rectangle();
r.Acceptdetails();
r.Display();
Console.ReadLine();
}
}
}
1.1 using 关键字
任何 C# 代码的开端,都是通过 using
引入本文件需要的命名空间, 就是导包
1.2 class 关键字
通过使用 class
来声明一个类
1.3 注释
注释可以分为单行注释和多行注释:
- 单行注释
通过//
进行注释 - 多行注释
通过下面的方式:
/**
* 这里是注释
* /
1.4 实例化
我们通过 new Rectangle()
来实例化一个矩形类, 因此和 java 一样,都是通过 new
来实例化。
1.5 成员变量和成员函数
如 length
、 width
是类 Rectangle
的成员变量。
如 GetArea
、 Display
是类 Rectangle
的成员函数。
1.6 标识符
标识符是分配给类型(类、接口、结构、记录、委托或枚举)、成员、变量或命名空间的名称。
在 C# 中,它们的命名必须遵循如下基本规则:
- 必须以字母、下划线或 @ 开头,后面可以跟一系列的字母、数字( 0 - 9 )、下划线( _ )、@。
- 第一个字符不能是数字。
- 不能包含任何嵌入的空格或符号,比如 ? - +! # % ^ & * ( ) [ ] { } . ; : " ’ / \。
- 不能是 C# 关键字,除非它们有一个 @ 前缀。 例如,@if 是有效的标识符,但 if 不是,因为 if 是关键字。
- 必须区分大小写。大写字母和小写字母被认为是不同的字母。
- 不能与C#的类库名称相同。
1.7 关键字
C# 保留着两种关键字:
- 保留关键字
静态的关键字,如private
、abstract
等这种修饰关键字 - 上下文关键字
在上下文中有特殊意义,如get
、set
,以后会学习到
二、 数据类型
在 C# 中,有以下三种数据类型:
- 值类型(Value types)
- 引用类型(Reference types)
- 指针类型(Pointer types)
2.1 值类型(也就是基本数据类型)
值类型可以直接进行值的分配,这些值都是从 System.ValueType
中派生出来的。这些值类型存储一个数据,这些数据有浮点数、整型、字符等。
下表为 C# 里的可用值类型:
我们可以通过 sizeOf
来打印这些类型在对应机器/平台上所占字节:
static void Main(string[] args)
{
Console.WriteLine("Size of int: {0}", sizeof(int));
}
// 打印: Size of int: 4, 说明 int 在平台上占 4 个字节
2.2 变量引用
引用类型不包含实际的数据,只包含对变量的引用。它有三种: object
、 string
、 dynamic
a. object / Object
System.Object
类型是 C# 的最顶层基类,所以对象类型可以分配以任意的类型。 不过在分配之前,可能会有装箱和拆箱的阶段。
- 装箱
将值类型(基本数据类型) 转化为对象类型 - 拆箱
将对象类型转化为值类型
如下面这段代码:
Object obj; // 也可以声明成: object obj
obj = 100; // 将 100 的 int 转化为一个对象
我们可以打印下 obj 最终的类型:
Console.WriteLine("type of obj: {0}", obj.GetType());
// 打印结果:type of obj: System.Int32
b. String
System.String
允许我们给其分配任意字符串数据,它是从 Object
派生出来的,其值可以通过两种形式进行分配:
- 引号
String str = "runoob.com";
- @修饰
@"runoob.com";
第二种的使用场景在对字符串进行转义时,会更加方便,例如下面代码:
string str = @"C:\Windows";
等价于:
string str = "C:\\Windows";
并且 @字符串中可以任意换行,换行符及缩进空格都计算在字符串长度之内:
string str = @"<script type=""text/javascript"">
<!--
-->
</script>";
c. dynamic
动态类型可以让我们直接存储任意值在该类型变量中,它会等到编译期才会去检查这个类型,同时我们也可以改变它:
static void Main(string[] args)
{
dynamic str = "aa";
Console.WriteLine("get str type {0}", str.GetType());
str = 12;
Console.WriteLine("get str type {0}", str.GetType());
}
// 打印:
get str type System.String
get str type System.Int32
看起来这是一个比较便利的赋值方式,但感觉会破坏代码的可读性。
2.3 指针引用
和 C、C++ 一样,C# 中的指针也是存储另一种类型的内存地址。
声明指针类型的语法:
type* identifier;
如:
char* cptr; // 指向一个字符的内存位置
int* iptr; // 指向一个整型的内存位置
三、 类型转化
C# 的类型分为 显示转化 和 隐式转化。
3.1 隐式类型转化
隐式转换是指:将一个较小范围的数据类型转换为较大范围的数据类型时,编译器会自动完成类型转换。
这些转换是 C# 默认的以安全方式进行的转换, 不会导致数据丢失。
例如:
- 从小的整数类型转换为大的整数类型
- 从派生类转换为基类
下面代码中,将一个 byte
类型(1个字节)的变量赋值给 int
类型(4个字节)的变量,编译器会自动将 byte
类型转换为 int
类型,不需要显示转换:
byte b = 10;
int i = b; // 隐式转换
3.2 显示类型转化(强制转化)
在代码上用显示的关键字将类型进行转化。
显式转换是指:将一个较大范围的数据类型转换为较小范围的数据类型时,或者将一个对象类型转换为另一个对象类型时,需要使用强制类型转换符号进行显示转换,强制转换会造成数据丢失。
例如,将一个 int
类型的变量赋值给 byte
类型的变量,需要显示转换。
int i = 10;
byte b = (byte)i; // 使用括号进行强转
下面是一个强转类的例子:
using System;
namespace TypeConversionApplication
{
class ExplicitConversion
{
static void Main(string[] args)
{
double d = 5673.74;
int i;
// 强制转换 double 为 int
i = (int)d;
Console.WriteLine(i);
Console.ReadKey();
}
}
}
// 将打印 5673
C# 内置了一些转化函数,我们可以通过函数去进行强转:
如下代码所示:
int i = 75;
float f = 53.005f;
double d = 2345.7652;
bool b = true;
Console.WriteLine(i.ToString());
Console.WriteLine(f.ToString());
Console.WriteLine(d.ToString());
Console.WriteLine(b.ToString());
// 打印:
75
53.005
2345.7652
True
四、 常量
常量是固定值,它可以是任意值类型数据(基本数据类型),还有枚举,例如常见的有:
- 整数常量
十进制、八进制、十六进制等整数数据 - 浮点数常量
整数、小数和指数所构成的浮点数 - 字符常量
单个字符,使用单引号 (‘x’) 进行修饰 - 字符串常量
多个字符构成,使用双引号 (“xxx”) 进行修饰 - 。。 等等其他基本数据类型
通过 const
关键字来修饰一个常量字符:
const int c1 = 5;
const int c2 = c1 + 5;
const string c3 = "rikka";
五、 省略 运算符、判断、循环 章节
这些用法和 java 如出一辙…
六、C# 的封装权限
C# 也有 private
、public
、protected
、internal
来修饰类、函数、变量,但是比较特殊的是它还有一个 protected internal
!
public
所有外部对象均可访问internal
同一个程序集(命名空间、包)的对象可以访问protected
只有本类和本类的子类可以访问private
仅本类可以访问protected internal
protected
和internal
的并集,即 本类、子类、命名空间内可访问。因此它是介于public
和internal
之间的访问权限
七、方法(method)
C# 定义方法为 method 而不是 function,就说明其入参是可变的~
其定义方法的方式和 java 一致,但不同点是,它的参数传递除了可以传递一般的值,还可以传递引用参数和输出参数。
方式 | 描述 |
---|---|
值参数 | 默认的方式,这种方式下将复制参数的实际值给函数,作为形式参数,实参和形参使用的是两个不同内存中的值。在这种情况下,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。 |
引用参数 | 这种方式复制参数的内存位置的引用给形式参数。这意味着,当形参的值发生改变时,同时也改变实参的值。 |
输出参数 | 这种方式可以返回多个值。 |
a. 传递值参数
class NumberManipulator
{
public void swap(int x, int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;
Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);
/* 调用函数来交换值 */
n.swap(a, b);
Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);
}
}
// 打印输出:
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:100
在交换之后,b 的值:200
b. 传递引用参数
给函数传递引用参数,意思是传递一个对变量的内存位置的引用,因此在函数体对它进行修改时,调用该函数的函数中,变量也会发生改变。
通过使用 ref
关键字来传入引用参数:
class NumberManipulator
{
public void swap(ref int x, ref int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;
Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);
/* 调用函数来交换值 */
n.swap(ref a, ref b);
Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);
}
}
// 打印:
在交换之前,a 的值: 100
在交换之前,b 的值: 200
在交换之后,a 的值: 200
在交换之后,b 的值: 100
c. 传递输出参数
一个方法只能返回一个数据,而通过传递输出参数,则函数能输出多个参数,而且和传递引用参数一样,方法能够改变传入参数的实际值。
使用 out
关键字来传递输出参数:
public void getValue(out int x)
{
int temp = 5;
x = temp; // 必须要处理 x,让它有个值,不然会报红
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
Console.WriteLine("在方法调用之前,a 的值: {0}", a);
/* 调用函数来获取值 */
n.getValue(out a);
Console.WriteLine("在方法调用之后,a 的值: {0}", a);
}
// 打印:
在方法调用之前,a 的值: 100
在方法调用之后,a 的值: 5
在举一个代码示例,我们可以不用输出参数进行初始化,直接定义后就传入,让函数体来计算出结果:
public void getValues(out int x, out int y )
{
Console.WriteLine("请输入第一个值: ");
x = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("请输入第二个值: ");
y = Convert.ToInt32(Console.ReadLine());
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a , b;
/* 调用函数来获取值 */
n.getValues(out a, out b);
Console.WriteLine("在方法调用之后,a 的值: {0}", a);
Console.WriteLine("在方法调用之后,b 的值: {0}", b);
}
// 打印:
请输入第一个值:
10
请输入第二个值:
200
在方法调用之后,a 的值: 10
在方法调用之后,b 的值: 200
八、可空类型
C# 没有 java 中那样的 @Nullable
、 @NonNullable
注解来强制声明数据是否可空,但是它也有类似的能力来进行标识。
8.1 C# 的单问号(?) 和双问号(??)
- 单问号
?
单问号用于对int
、double
、bool
这种不可以直接赋值为 null 的基本数据类型,它可以让这个值可空:
int? i = 3;
// 等价于
Nullable<int> i = new Nullable<int>(3)
int i; //默认值0
int? ii; //默认值null
- 双问号
用于判断一个变量在为 null 的时候返回一个指定的值。
double? num1 = null;
double num2 = num1 ?? 5.3; // num1 为空时,赋值5.43,
8.2 单问号 - C# 可空类型(Nullable)
C# 提供了一个特殊的数据类型,nullable
类型(可空类型),通过加上单问号,可以表示其基础值类型正常范围内的值,再加上一个 null 值。
例如 bool?
可以提供 true、false 、null。
声明一个 nullable 类型(可空类型)的语法如下:
<data_type>? <variable_name> = null;
例如下面代码:
int? num1 = null;
int? num2 = 45;
double? num3 = new double?();
double? num4 = 3.14157;
bool? boolval = new bool?();
// 显示值
Console.WriteLine("显示可空类型的值: {0}, {1}, {2}, {3}", num1, num2, num3, num4);
Console.WriteLine("一个可空的布尔值: {0}", boolval);
// 打印:
显示可空类型的值: , 45, , 3.14157
一个可空的布尔值:
8.3 双问号 - Null 合并运算符 ??
??
是一个二元运算符,又称合并运算符,类似于 Kotlin 的 Elvis (?:
) 运算符。
double? num1 = null;
double? num2 = 3.14157;
double num3;
num3 = num1 ?? 5.34; // num1 如果为空值则返回 5.34
Console.WriteLine("num3 的值: {0}", num3);
num3 = num2 ?? 5.34;
Console.WriteLine("num3 的值: {0}", num3);
因为 C# 也有和 java 一样的 bool ? a : b
的三目运算符,因此 ??
其实是它的一个简化版本:
num3 = num1 ?? 5.34;
// 等价于
num3 = num1 != null ? num1.Value : 5.34d; // IDE 遇到这个情况,会提示你优化成上面那样
九、数组
在 C# 中,我们通过 datatype[] arrayName;
来声明一个可变数组。例如:
// 声明一个大小为 10 的双浮点数组
double[] myDoubleArray = new double[10];
并且可以直接进行赋值:
double[] myDoubleArray = new double[10];
myDoubleArray[0] = 0.1;
myDoubleArray[10] = 0.2; // 运行时报错,数组越界
我们还可以通过别的方式来初始化一个数组:
double[] balance = { 2340.0, 4523.69, 3421.0}; // 在花括号中定义初始值
int [] marks = new int[5] { 99, 98, 92, 97, 95}; // 定义大小并初始化
int[] score = marks; // 也可以将一个数组直接赋值给另一个数组
最后, C# 提供 foreach
方法来帮助我们高效访问数组中的元素:
int [] n = new int[10]; /* n 是一个带有 10 个整数的数组 */
/* 使用原始的 for 循环来初始化数组 n 中的元素 */
for ( int i = 0; i < 10; i++ )
{
n[i] = i + 100;
}
// 使用foreachx循环
foreach (int j in n )
{
int i = j-100;
Console.WriteLine("Element[{0}] = {1}", i, j);
}
// 打印:
Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
...
当然了,除了这些,C# 还提供了一些“花样”的数组,下面让我们来学习它们。
9.1 多维数组
C# 可以支持 二维、三维… n维数组:
string [,] names; // 二维字符串数组
int [ , , ] m; // 三维整型数组
// 初始化一个二维数组
int [,] a = new int [3,4] {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
9.2 交错数组
就像 Kotlin、Java 中 List<List<Int>>
, 是数组的数组,也被称为交错数组。
在 C# 中使用连续多个中括号来声明,例如:
int[][] a; // Kotlin 中的 List<List<Int>>
string[][][] b; // Kotlin 中的 List<List<List<String>>>
9.3 传递参数数组(可变长入参)
有时,当声明一个方法时,我不能确定要传递给函数作为参数的参数数目。C# 参数数组解决了这个问题,参数数组通常用于传递未知数量的参数给函数。类似于 Kotlin 中的 vararg,在 C# 中则使用 params
关键字:
// 声明方式
public 返回类型 方法名称( params 类型名称[] 数组名称 )
class ParamArray
{
public int AddElements(params int[] arr) // 可以传递不确定数量的 int
{
int sum = 0;
foreach (int i in arr)
{
sum += i;
}
return sum;
}
}
class TestClass
{
static void Main(string[] args)
{
ParamArray app = new ParamArray();
int sum = app.AddElements(512, 720, 250, 567, 889);
Console.WriteLine("总和是: {0}", sum);
Console.ReadKey();
}
}
十、结构体(struct)
就像 Kotlin 中的 data class
一样, C# 也有结构体 struct
:
struct Books // 定义一个书本的结构体,可以声明与书相关的数据和函数
{
public string title;
public string author;
public string subject;
public int book_id;
};
Books myBook1 = new Books();
// 即使不用 new ,也可以被实例化
Books myBook2;
myBook2.book_id = 1;
但是它和普通 class
有很多不同点,先来看看其特点。
10.1 结构体的特点
- 可带有方法、字段、索引、属性、运算符方法和事件。
- 可定义构造函数,但不能定义析构函数(学习类时会讲)。但是,您不能为结构定义无参构造函数。无参构造函数(默认)是自动定义的,且不能被改变。
- 与类不同,结构不能继承其他的结构或类。
- 不能作为其他结构或类的基础结构。
- 结构可实现一个或多个接口。
- 结构成员不能指定为
abstract
、virtual
或protected
。 - 当使用
new
操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用new
操作符即可被实例化。 - 如果不使用
new
操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。建议还是用new
来实例化
10.2 结构体与类的区别
- 结构体是值类型,它在栈中分配空间;而类是引用类型,它在堆中分配空间,栈中保存的只是引用。
因为栈的读写速度快于堆,因此结构体的性能高于类(在成员不是很多的情况下) - 结构类型直接存储成员数据,让其他类的数据位于堆中,位于栈中的变量保存的是指向堆中数据对象的引用。
- 结构体中声明的字段无法赋予初值,类可以.
结构体和类的适用场合分析:
- 当堆栈的空间很有限,且有大量的逻辑对象时,创建类要比创建结构体好一些;
- 对于点、矩形和颜色这样的轻量对象,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构体的成本较低;
- 在表现抽象和多级别的对象层次时,类是最好的选择,因为结构体不支持继承。
- 大多数情况下,目标类型只是含有一些数据,或者以数据为主,选择结构体
十一、类
没有什么需要学习的,唯一要注意的是,和 C语言一样, C# 的类也有析构函数。
析构函数是在对象被移除堆栈时调用的,因此适合用来做资源释放,无法被继承和重载。在 C# 中使用 ~
来开启析构函数:
~Line() //析构函数
{
Console.WriteLine("对象已删除");
}
11. 1 继承与重写
C# 中只能单继承,使用 :
关键字来继承:
abstract class Shape
{
public void setWidth(int w)
{
width = w;
}
public void setHeight(int h)
{
height = h;
}
protected int width;
protected int height;
abstract public int Area();
}
// 派生类
class Rectangle: Shape
{
public int getArea()
{
return (width * height);
}
public override int Area()
{
Console.WriteLine("Rectangle 类的面积:");
return GetArea();
}
}
11.2 虚方法
当有一个定义在类中的函数需要在继承类中实现时,可以使用虚方法 virtual
。
虚方法 和 抽象类 组成了 C# 的多态。下面看下虚方法的运用:
public class Shape
{
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }
// 虚方法
public virtual void Draw()
{
Console.WriteLine("执行基类的画图任务");
}
}
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个圆形");
base.Draw();
}
}
class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("画一个长方形");
base.Draw();
}
}
11.3 运算符重载
和 Kotlin 一样,可以去重载运算符,通过 operator
关键字,例如下面代码,将重载 Box
类的加法运算符:
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;
}
11.4 接口实现
通过使用 interface
来定义接口,而实现类在实现时,无需 override
关键字:
interface MyInterface1
{
void myFunction1();
}
interface MyInterface2
{
void myFunction2();
}
class MyClass : MyInterface1, MyInterface2 // 多实现
{
void MyInterface1.myFunction1()
{
}
void MyInterface2.myFunction2()
{
}
}
十二、 预处理指令
预处理器指令指导编译器在实际编译开始之前对信息进行预处理。
所有的预处理器指令都是以 #
开始。预处理器指令不是语句,所以它们不以分号(;)结束。
C# 编译器没有一个单独的预处理器,但是,指令被处理时就像是有一个单独的预处理器一样。在 C# 中,预处理器指令用于在条件编译中起作用。一个预处理器指令必须是该行上的唯一指令。
下表为 C# 中预处理指令列表:
那预处理指令的作用是什么呢?
实际上它在程序调试和运行上有重要的作用。
比如预处理器指令可以禁止编译器编译代码的某一部分,如果计划发布两个版本的代码,即基本版本和有更多功能的企业版本,就可以使用这些预处理器指令来控制。在编译软件的基本版本时,使用预处理器指令还可以禁止编译器编译于额外功能相关的代码。另外,在编写提供调试信息的代码时,也可以使用预处理器指令进行控制。总的来说和普通的控制语句(if等)功能类似,方便在于预处理器指令包含的未执行部分是不需要编译的。
12.1 #define 预处理器
#define
预处理器指令创建符号常量。
#define
允许定义一个符号,这样,通过使用符号作为传递给 #if
指令的表达式,表达式将返回 true。它的使用:
#define PI // 定义 PI
using System;
namespace PreprocessorDAppl
{
class Program
{
static void Main(string[] args)
{
#if (PI) // 是否有定义 PI, 因为上面定义了,所以这条会被编译
Console.WriteLine("PI is defined");
#else // 因为 PI 已定义,所以这下面的代码不会被编译,相当于做了编译优化
Console.WriteLine("PI is not defined");
#endif
}
}
}
// 执行: PI is defined
12.2 条件指令
可以使用 #if
指令来创建一个条件指令。条件指令用于测试符号是否为真。如果为真,编译器会执行 #if
和下一个指令之间的代码,例如下面代码中,在不同的环境下来实现不同的功能
#define DEBUG // 这里可能定义在别的地方,如总配置文件中 Configer
#define VC_V10 // 这里可能定义在别的地方
using System;
public class TestClass
{
public static void Main()
{
#if (DEBUG && !VC_V10)
Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && VC_V10)
Console.WriteLine("VC_V10 is defined");
#elif (DEBUG && VC_V10)
Console.WriteLine("DEBUG and VC_V10 are defined");
#else
Console.WriteLine("DEBUG and VC_V10 are not defined");
#endif
}
}