C# 学习笔记
- Chapter 1 C# 基础部分
- Section 1 类与命名空间
- Part 1 命名空间 NameSpace
- Part 2 类 Class
- Section 2 基本元素
- Section 3 数据类型
- Part 1 什么是类型?
- Part 2 类型在 C Sharp 中的作用
- Part 3 C Sharp 中的数据类型
- Section 4 变量、对象与内存
- Part 1 变量
- Part 2 如何声明变量
- Part 3 值类型变量与内存
- Part 4 引用类型变量与引用内存实例与内存
- Part 5 其他
- Section 5 方法的定义、调用与调试
- Part 1 方法
- Part 2 方法的声明与调用
- Part 3 构造器/构造函数
- Part 4 方法的重载 Overload
- Part 5 如何对方法进行 Debug
- Breakpoint
- Step-in,Step-over,Step-out
- Section 6 操作符详解(操作符原理与使用)
- Part 1 操作符概览
- Part 2 优先级与运算顺序
- Part 3 操作符示例
Chapter 1 C# 基础部分
Section 1 类与命名空间
类库的引用,是使用命名空间的物理基础,不同的技术类型的项目会默认应用不同的类库
- DLL 引用,黑盒引用,无源代码;
- 项目引用,白盒引用,有源代码;
Part 1 命名空间 NameSpace
命名空间的目的是提供一种让一组名称与其他名称分隔开的方式,在一个命名空间中声明的类的名称与另一个命名空间中声明的相同的类的名称不冲突。
声明方法如下:
namespace namespace_name
{// 代码声明
}
using关键字表明程序使用的是给定命名空间中的名称,例如使用 System 命名空间中的Console类,可以这样写:
Console.WriteLine();
如果没有使用 using 关键字,则可以写完全限定名称:
System.Console.WriteLine();
命名空间是可以嵌套的,使用点( . )运算符访问嵌套的命名空间的成员;
Part 2 类 Class
依赖关系: 类或对象之间的耦合关系应追求 “高内聚,低耦合” 的原则。
什么是类:
- 类 Class 是现实世界事物的模型;
- 类与对象的关系
-
- 什么时候叫“对象”,什么时候叫“实例” : 对象与实例是一回事儿,是类经过 实例化 后得到的内存中的实体;但有些类是不能实例化的;实例化是依照某个类,创建对象,就是实例化。抽象的说就是魔鬼附体,魔鬼是虚拟的,附体以后是实体,就是实例化;使用 new 操作符创建类的实例;
-
- 引用变量与实例的关系:引用变量这个概念是非常重要的,使用引用变量的方法,可以连续操作同一个实例,如下所示;
Form myForm;
myForm = new From();
myForm2 = myForm; // 二者引用的是同一个实例,操作同一个实例;
- 类的三大成员
-
- 属性 Property:专门用于存储数据,数据组合起来可以表示类或对象当前的状态,也有称作“字段";
-
- 方法 Method:表示类或对象可以做什么,就是函数,就是算法;
-
- 事件 Event:类或对象通知其他类或对象的机制,为C#所持有
- 类的静态成员与实例成员:静态 (Static) 成员在语义上表示它是“类的成员”,实例 (非静态) 成员在语义表示它是“对象的成员”,不是属于某个类;
-
- 关于绑定 Binding:指的是编译器把一个成员与类或对象关联起来;
Section 2 基本元素
构成 C# 语言的基本元素
- 关键字 Keyword
- 操作符 Operator:用于表达运算思想的符号;
- 标识符 Identifier:名字,必须以字符或下划线开头,C Sharp大小写敏感,变量名用驼峰法(类似 myForm 这种写法),方法名用Pascal法(所有的单词首字母大学)
- 标点符号
- 文本
- 注释与空白
前五项统称为标记 Token,也就是对于编译器而言是有用的
Section 3 数据类型
Part 1 什么是类型?
- 类型 Type,也叫做数据类型 Data Type,是性质相同的值的集合,且配备了一系列专门针对这种类型的值的操作。
- 是数据在内存中储存时的型号;
- 小内存容纳大尺寸数据会丢失精度、发生错误;
- 大内存容纳小尺寸数据会导致浪费;
- 编程语言的数据类型与数据的数据类型不完全相同;
Part 2 类型在 C Sharp 中的作用
一个C#类型中所包含的信息由:
- 储存此类型变量所需的内存空间大小;
- 此类型的值可表示的最大、最小值范围
- 此类型所包含的成员(如方法、属性、事件等);
- 此类型由何基类派生而来
- 程序运行的时候,此类型的变量在分配在内存的什么位置
- Stack简介
- Stack Overflow
- Heap简介
- 使用Performance Monitor查看进程的堆内存使用量
- 关于内存泄漏
- 此类型所允许的操作(运算)
Type | Range | Size |
---|---|---|
sbyte | -128 to 127 | Signed 8-bit integer |
byte | 0 to 255 | Unsigned 8-bit integer |
short | -32,768 to 32,767 | Signed 16-bit integer |
ushort | 0 to 65,535 | Unsigned 16-bit integer |
int | -2,147,483,648 to 2,147,483,647 | Signed 32-bit integer |
uint | 0 to 4,294,967,295 | Unsigned 32-bit integer |
long | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | Signed 64-bit integer |
ulong | 0 to 18,446,744,073,709,551,615 | Unsigned 64-bit integer |
C# type/keyword | Approximate range | Precision | Size | .NET type |
---|---|---|---|---|
float | ±1.5 x 10^−45 to ±3.4 x 10^38 | ~6-9 digits | 4 bytes | System.Single |
double | ±5.0 × 10^−324 to ±1.7 × 10^308 | ~15-17 digits | 8 bytes | System.Double |
decimal | ±1.0 x 10^-28 to ±7.9228 x 10^28 | 28-29 digits | 16 bytes | System.Decimal |
程序在写代码、编译时为静态时期,运行调试为动态时期
Stack 栈,是给方法调用使用的;Heap 堆,是用来存储对象的;
一般栈比较小,堆比较大;
程序在堆里面分配对象,使用完后没有回收,浪费掉内存,也叫做内存泄漏;
在C Sharp里面没有手动回收,有自动机制回收;
// 例子:
class BadGuy
{public void BadMethod(){int x = 100;this.BadMethod();// 只递不归,会导致栈爆掉 Stack Overflow}
}// int 有7为总共2097152字节,下面的例子会导致栈爆掉,编译无问题但会 Stack Overflow
unsafe
{ int* p = stackalloc int[9999999];
}
Process 进程:程序运行后的实例,运行后有程序ID,叫做PID
使用 perfmon 打开性能监视器
Part 3 C Sharp 中的数据类型
C Sharp 中的五大数据类型
- 类 Classes 例如Windows,Form,Console,String (对于初学者用的最多)
- 结构体 Structures 例如 Int32,Int64,Single,Double(对于初学者用的最多)
- 枚举 Enumerations 例如 HorizontalAlignment,Visibility
- 接口 Interface
- 委托 Delegates
类使用 class 关键字声明;
结构体使用 struct 关键字声明;
枚举类型,给定一个集合,用户只能从集合中选一个值,不能随意选值,也就是里面的数据都是预定义好的选项,使用 enum
关键字声明的类型就是枚举类型,例如下面的例子;
namespace System.Windows.Forms
{public enum FormWindowState{Normal = 0,Minimized = 1,Maximized = 2,}
}
由这些数据类型构成了 C# 的数据类型系统,如下图:
类,接口,委托归为引用类型;结构体和枚举归为值类型;所有的类型都以 Object 类型为基类型;
第一组对应引用类型,object 和 string 是真正的数据类型,有对应的类,横线向下的 class 、interface和delegate不是具体的数据类型,而是引用三个关键字定义自己的数据类型;
第二组对应值类型,横线上方的蓝字关键字,横线下struct定义结构体,enum定义枚举;
第三组的true和false是布尔类型的值;void表示函数不需要返回值,null表示引用变量里面的值是空的,不引用任何实例;var和dynamic用来声明变量;
途中的蓝字表明都是现成的数据类型,且这些数据类型非常常用,被C#吸收成为关键字,且这些数据类型为基本数据类型,也就是别的数据类型由这些基本数据类型构成;
Section 4 变量、对象与内存
在 C# 语言类型系统中,引用类型和值类型的变量在内存中的存储是不一样的,有自己的特点,如果二者没有分清,容易出错;
Part 1 变量
什么是变量
- 表面上来看,也就是从C#代码的上下文行文上来看,变量的用途是储存数据;
int x = 100
x =100;
// 表面上就是将标准整数的值 100 通过等号赋值给 x;
// 实际上,x 就是一个标签,标签对应内存中的地址,100 这个值就存在内存中的这个地址中;
// x 是一个整数类型,int32 指的就是 32 个比特位,4 个字节存储这个值,也就是 int 类
// 型的值才能存入这个地址,内存只给分配 4 个字节,内存空间只能存相同或较小的值,后者会造成浪费;
- 实际上,变量表示了储存位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量;
-
- 变量名表示(对应着)变量的值在内存中的储存位置;
-
- 变量类型告诉计算机系统这段内存用来保存这个变量的值,用来保存的值若能存入这段内存,则可以,若不能装入这段内存,编译器会报错;
- 在 C# 中有 7 中变量(狭义的“变量”,通常指局部变量)
-
- 静态变量
-
- 实例变量(成员变量,字段)
-
- 数组元素
-
- 值参数
-
- 引用参数
-
- 输出形参
-
- 局部变量:声明在方法体(函数体)里面的变量
上述7中变量在程序中的写法与用法定义
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student student = new Student();student.Age = -1; // 字段裸露在外容易不小心被赋予一个不合理的值}}class Student{// 静态成员变量, 隶属于这个类,不属于这个类的实例,字段裸露在外容易不小心被赋予一个不合理的值public static int Amount;// 字段,属性的雏形,public int Age;public string Name;// 数组类型int[] arrays = new int[100];// 声明了一个长度为100的整型数组,一个数占4个字节,这个数组也就是400字节的长度// 值参数,例如: double a, double bpublic double Add(double a, double b){ // 局部变量 例如 result 声明在 Add 函数体当中,// 那么 result 就是函数体 Add 的局部变量double result = a + b;return result;}// 引用参数变量,在参数前加上 ref,例如: ref double apublic double Add(ref double a, double b){return a + b;}// 输出参数变量,在参数前加上 out,例如: out double apublic double Add(out double a, double b){return a + b;}}
}
Part 2 如何声明变量
int a; // 告诉编译器这个参数是存在的,简单的声明变量
a = 100;
int b;
b = 200
int c = a + b;
声明变量的正确格式如下:opt 表示可选的
有效的修饰符组合(opt) 变量类型 变量名(必须是个名词) 初始化器(opt)
例如声明类:
class Student
{public static int Amount = 0; // public static 就是有效的修饰符组合,如果是 public private static 就是无效的修饰符组合// int 是变量类型;// Amount 就是变量名;// 后面的等号和一个值就是初始化器;
}
变量的定义:
变量:以变量名所对应的内存地址为起点、以其数据类型所要求的储存空间为长度的一块内存区域
Part 3 值类型变量与内存
内存的最小单位是比特,八个比特位组成一个字节,计算机内存中以字节为基本单元存取数据。计算机为每个字节都准备了唯一编号,内存地址指的是字节在计算机中的编号。
下图橙色区域为操作系统占用的内存区域,右侧为自由内存区域;
例如
byte b;
黄色区域为其他程序占用,而10000015正好空出,则给 b 这个变量;
byte b;
b = 100;
100 的二进制为 1100100 只有七位,差一位最高位补0,在内存中如下存储:
若使用带有符号的变量,高位作为符号位,其余位数用于存储数据,若值为负数,每一位都按位取反,从末尾加1;
值类型的变量是没有实例的,所谓的实例与变量合而为一的。
Part 4 引用类型变量与引用内存实例与内存
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student stu;}}class Student{// ID 与 Score 为值类型的变量uint ID;ushort Score;}
}
下图中橙色与黄色区域为其他程序所占用,值类型是按照实际的引用大小分配内存,而引用类型并非如此。
Student 的实例在内存中,uint 4字节,ushort 2 字节,在内存中并非直接分6字节,计算机看到是引用类型,直接分四个字节,32 比特,这些地方全都刷成0,表示没有引用任何实例。
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student stu;stu = new Student(); // 后面半句表示在堆内存里创建 Student 实例}}class Student{// ID 与 Score 为值类型的变量uint ID;ushort Score;}
}
分配完实例之后,将堆内存的地址保存在 stu 的变量里。堆里创建实例,也需要寻找空闲内存,分配空间。一个字段ID占四个字节,另外一个字段Score 占两个字节,一共需要六个字节,于是在空闲区域分配六个字节给实例,前四个给uint,后两个给ushort。
分得的地址 30000001 转为二进制: 1,11001001,11000011,10000001
前八位 10000001
之后是11000011
再之后是11001001
剩下一位 1,高位补0,也就是 00000001
这四组值安高低原则存在内存中
引用变量里存的值,是实例所在的堆内存的地址的值,引用变量所存的值是实例在堆内存上的地址,确定了引用关系。
引用类型变量与实例的关系:引用类型变量里储存的数据是对象的内存地址。
Part 5 其他
- 局部变量是在 Stack 上分配内存;
- 实例变量,也就是字段,会随着字段在堆上分配内存;
- 变量的默认值,一旦变量在内存中分配好了,未赋值时,内存块都为0,但局部变量必须赋值才能编译通过;
- 常量,值不可改变的量,类似Java中的final;
- 装箱与拆箱(Boxing & Unboxing)
Section 5 方法的定义、调用与调试
Part 1 方法
- 方法Method的前身是C/C++语言的函数function
- 方法是面向对象范畴的概念,在非面向对象语言中仍然成为函数;
- 方法永远都是类或结构体的成员,C#中的函数不可能独立于类(或结构体)之外,只有作为类(或结构体)的成员时才被称为方法;
- 方法是类(或结构体)最基本的成员之一,最基本的成员只有两个:字段与方法(成员变量与成员函数),本质还是数据+算法构成的;方法表示类(或结构体)能做什么事情;
写程序的时候为什么需要方法和函数呢:
- 隐藏复杂的逻辑
- 把大算法分解为小算法
- 复用 reuse
Part 2 方法的声明与调用
- 在 C# 中方法的声明与定义不分家的
- 参数 Parameter 全称 formal Parameter 形式上的参数,简称形参
- 声明方法要有方法头(可选特性,有效修饰符组合,返回值类型,方法名称{动词短语},可选的类型参数列表{泛型才有},后面跟的圆括号里面要有形式参数列表,可选的对类型参数的约束)和方法体(要么是语句块要么是分号);
- 方法命名需要大小写规范,Pascal命名,需要以动词或动词短语作为名字
- 方法调用时,在方法名后面跟上一对圆括号,在圆括号内写入必要的参数,这个圆括号不可省略,在圆括号内写入的是实际参数,简称实参 Argument,可以理解为调用方法时的真实条件;
- 调用方法时的 Argument 列表要与定义方法时的Parameter列表相匹配,值与变量需要匹配,数量和类型都要匹配;
Part 3 构造器/构造函数
构造器
- Constructor 是类型的成员之一
- 狭义的构造器指的是 “实例构造器 instance constructor ” 构建实例在内存当中的内部结构;
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student student = new Student(); // 这个后面Student后面的括号就是在调用构造器Console.WriteLine(student.ID); // 运行之后会有一个值,见下图,表明默认构造器起作用了}}// 当声明一个类,又没有准备构造器,编译器会准备一个默认的构造器class Student{public int ID;public string Name;}
}
如果使用自定义的构造器:
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student student = new Student();Console.WriteLine(student.ID);}}class Student{// 自定义的,不带参数的构造器public Student(){this.ID = 1;this.Name = "No Name";}public int ID;public string Name;}
}
为了在初始化时给变量赋值
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student student = new Student(initID: 12345, initName: "Hello World");Console.WriteLine(student.ID);Console.WriteLine(student.Name);}}class Student{// 自定义的构造器public Student(int initID, string initName){this.ID = initID;this.Name = initName;}public int ID;public string Name;}
}
两个构造器运行的结果是不同的:
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student student = new Student(initID: 12345, initName: "Hello World");Console.WriteLine(student.ID);Console.WriteLine(student.Name);Console.WriteLine("========================");Student student1 = new Student();Console.WriteLine(student1.ID);Console.WriteLine(student1.Name);}}class Student{// 自定义的构造器public Student(int initID, string initName){this.ID = initID;this.Name = initName;}public Student(){this.ID = 1;this.Name = "No Name";}public int ID;public string Name;}
}
个人感觉 this 的用法和 python 中 self 用法很像,构造器也类似 python 中类的初始化部分;
Part 4 方法的重载 Overload
在两个方法名字完全一致的情况下,方法签名不能一样
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Console.WriteLine("Hello");Console.WriteLine(100);Console.WriteLine(200L);Console.WriteLine(300D);}}}
可以看到 Console.WirteLine
这个方法有17个Overload,以保证输入不同的数据都可以在命令行界面打印出来,这个是重载的一个简单体验,和简单的认知;
什么是方法的重载:一个类的方法名字可以完全一样,但方法签名不能完全一样
- 方法签名 Method signature 不能一样,这个是由方法、类型形参的个数和它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成,方法签名不包含返回值;
- 实例构造函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成;
- 重载决策(到底调用哪一个重载):用于在给定了参数列表的一组候选函数成员的情况下,选择一个最佳函数成员来实施调用;
下面的例子是如何声明带有重载的方法:
class Calculator
{public int Add(int a, int b){ return a + b;}public int Add<T>(int a, int b){T t; // 类型形参,未来可能会有这个类型参与到方法中return a + b;}public int Add(ref int a, int b){return a + b;}public int Add(int a, int b, int c) {return a + b + c;}public double Add(double a, double b){return a + b;}
}
下面的例子是解释重载决策:
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Calculator c = new Calculator();int x = c.Add(100, 100);Console.WriteLine(x);double y = c.Add(100D, 1200D);Console.WriteLine(y);}}class Calculator{public int Add(int a, int b){ return a + b;}public int Add<T>(int a, int b){T t; // 类型形参,未来可能会有这个类型参与到方法中return a + b;}public int Add(ref int a, int b){return a + b;}public int Add(int a, int b, int c) {return a + b + c;}public double Add(double a, double b){return a + b;}}
}
Part 5 如何对方法进行 Debug
- 设置断点 Breakpoint
- 观察方法调用时的 Call Stack
- Step-in,Step-over,Step-out
- 观察局部变量的值与变化
Breakpoint
在需要的地方打断点,就是白色区域的红色小点,对应的那一行代码会出现红色标红区域,运行后,当程序执行到断点区域,程序会暂停运行,同时将鼠标放置在想要查看的变量上方,会显示变量当前的值。在Visual Studio 当中,左下角会显示这个断点的地方里面的全部变量的值。这个就是断点的作用。
通过下图展示的 Call Stack 就可以看到当前调用的函数,最顶层是当前的函数,再往下是调用这个函数的函数,以此类推。
Step-in,Step-over,Step-out
在 Visual studio 中的工具栏部分,从左到右分别为Step-into (F11键),Step-over,Step-out
Step-into 代表走进代码当中,一步一步看程序的运行步骤,类似于看流程,看流程的过程中是从断点开始,可以一步一步看变量的数值。
Step-over 代表跳过方法的具体方法,直接看结果;
Step-out 代表调回断点的地方;
Section 6 操作符详解(操作符原理与使用)
Part 1 操作符概览
- 上图展示的是 C# 语言中所有的操作符,每一种操作符都有一种独特的运算;
- 操作符 Operator 也叫做运算符;
- 操作符是用来操作数据的,被操作符操作的数据称为操作数 Operand;
- 上图中,表格从上到下,优先级依次降低,也就是越靠上优先级越高,除最后一行之外的其他行,行内的内容优先级一致,当这些操作符组合起来时,从上到下依次运算,除最后一行外,同一行的操作符正常从左到右依次运算(先运算左边的表达式,再运算右边的表达式),最后一行相反,同一行操作符从右向左依次运算(先运算右边的表达式,再运行左边的表达式);
- 使用操作符时需要注意数值提升的问题;
C# 操作符的本质
- 操作符的本质是函数(即算法)的“简记法”;
- 操作符不能脱离与它关联的数据类型;
-
- 可以说操作符就是与固定数据类型相关联的一套基本算法的简记法,示例如下图所示;
创建一个简单的例子如下图所示:
创建了一个 Person,使用GetMarry函数连续相加两个形参,最终返回一个Person类型的列表;可以看到输出了十一行的结果;将GetMarry换成 operator关键字 “+” 这个形式,那么原先的方法就没了,将上文换成person1 + person2,运行后的结果与原先的方法的结果一致;
这个例子说明了,C#中的操作符就是方法、函数的简记法。
Part 2 优先级与运算顺序
- 在进行运算的时候,可以通过加圆括号的方式提高表达式的运行优先级,且圆括号可以嵌套的,最内层的算式最先运算
- 同优先级操作符的运算顺序,除了带有赋值功能的操作符,同优先级操作符都是从左向右进行运算的。
- 带有赋值功能的运算符,都是从右向左运算,示例如下;
int x;
x = 3 + 4 + 5; // = 是赋值操作符,具有赋值功能的,先计算右侧部分的算式,// 右侧部分的算式由左向右计算,最终将值赋值给 x;
- 在计算机语言里,计算机语言的同优先级运算没有“结合律”;
在类后面有一对尖括号,说明类为泛型类,泛型类不是一个完整的类,需要和其他的类组合才能成为一个完整的类;
Part 3 操作符示例
var 关键字,是与变量有关,声明隐式类型的变量,
int x; // 显式,告诉编译器这个变量的数据类型
var y; // 隐式,类型暂时不知道,当赋值的时候确定数据类型,也就是编译器有自动类型推导;
new操作符的作用:
- 在内存中创建一个类型的实例
- 并立刻调用这个实例的实例构造器
new Form(); // 就在内存中创建了这个实例,调用了这个实例默认的实例构造器
//===================================
// 将实例的内存地址赋值给变量,就可以通过变量访问这个实例,通常使用这种方法创建实例;
Form myForm = new Form();
myForm.Text = "Hello";
myForm.ShowDialog();
//===================================
// 除了调用实例构造器(),还可以调用实例的初始化器{},初始化器还可以初始化多个属性
Form myForm = new Form() {Text = "Hello", FormBorderStyle = FormBorderStyle.SizableToolWindow};
myForm.ShowDialog();
//===================================
// 为匿名类型创建
Form myForm = new Form(){Text = "Hello"}; // 针对非匿名类型
var person = new {Name = "Mr.OK", Age = 34}; // 匿名类型创建实例,使用 var 自动推断类型
// var + new 操作符的组合使用,为匿名类型创建对象,并且用隐式类型变量引用实例;
new 操作符功能强大,不能乱用,在写大型工程的时候使用依赖注入的方式降低耦合;
new 关键字的多用性,除了操作符外,还有别的用法,就不是操作符了;
如下图,可以看到有两行 I’m a student,因为CsStudent继承于父类Student,也就是把父类的Report方法继承下来;
如图,此时new为修饰符,不是操作符,是子类对父类方法的隐藏(不多见)
checked与unchecked操作符的作用
- 与检查一个值在内存中是否有溢出;
- Checked关键字用于告诉编译器检查有没有溢出;
- unchecked关键字用于告诉编译器不需要检查有没有溢出;
设置变量x为uint类型并赋予该类型最大值,再给x+1,使用 checked 检查是否有溢出,使用 try catch 方法捕获异常,可以看到捕获到了溢出;
这里使用unchecked,不检查溢出,可以看到在x+1后,所有位数都进1,于是这个值变成了0,也就是溢出;C#语言默认采用的使unchecked方法。
上述的两种用法,是作为操作符的用法,还有一种用法是上下文用法,如下图所示,使用checked方法,其内部的语句块会检测内部是否有溢出,uncheck的用法类似:
delegate操作符
通常在C#中作为委托使用,该用法比较少见;
使用delegate操作符声明匿名方法;
若不想让方法被外界调用且只调用一次,就使用匿名方法;
现在通常使用lambda表达式,来表达,数据类型可以省略,如下图所示:编译器会自动匹配数据类型。
sizeof操作符
- 获取尺寸
- 获取一个对象在内存当中所占字节数的尺寸;
- 默认情况下,只能获取基本数据类型的实例在内存中所占字节数,除了string和object,只能获取结构体数据类型的实例在内存中所占字节数;
- 非默认情况下,可以使用sizeof获取自定义的结构体类型的实例在内存中的所占字节数量,但是需要放在不安全的上下文当中
-> 操作符
指针操作,只能操作结构体类型,不能操作引用类型;
可以通过指针访问对象,但只能在unsafe中使用;
(T)x强制类型转换
类型转换:
- 隐式implicit类型转换
-
- 不丢失精度的转换;
-
- 子类向父类的转换;
-
- 装箱;
- 显式explicit类型转换
-
- 有可能丢失精度(甚至发生错误)的转换,即cast;
-
- 拆箱;
-
- 使用Covert类;
-
- ToString方法与各数据类型的Parse/TryParse方法
- 自定义类型转换操作符
什么是类型转换,看下面的例子:
Console.ReadLine() 方法返回的是 String 类型的,而我输入两个数字,希望计算这两个数字的和,但是目前输出的是两个字符串的组合;
这里使用了 Convert 进行数据类型转换,说明了数据类型转换的重要性;
下面开始正式的介绍:
隐式类型转换
不丢失精度的转换----------------------
上图展示的就是不丢失精度的隐式类型转换,因为int类型4个字节,long类型8个字节,完全能装的进去;
下图就是不丢失精度的隐式数值转换,也就是下图从右边向左边转换就会丢失精度;
子类向父类的转换----------------------
下面是一个例子:
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Teacher t = new Teacher();Human h = t;t.Teach(); // 可以看到 Teach 方法h.Think(); // 看不到 Teach 方法// 当试图用一个引用变量去访问所引用实例的成员的时候,//只能访问这个变量的类型所具有的成员,而不是这个变量所引用的实例的类型;// h 类型为 Human,有两个方法,没有Teach,所以看不到 Teach;// 这就是由子类向父类的隐式类型转换;}}class Animal{public void Eat(){Console.WriteLine("Eating");}}class Human : Animal{public void Think(){Console.WriteLine("Who I am?");}}class Teacher : Human{public void Teach(){Console.WriteLine("I teach programming");}}
}
显式类型转换
为什么会有显式类型转换?
因为显式类型转换有可能会导致精度的丢失或者发生错误,这个类似编译器推卸责任用的,编写者明确可能会丢失精度但也要转换类型;
Cast ,也就是 (T)x,方法,下面是例子来说明:
上图展示的使把一个32位的数据放进一个16位的空间,只能舍去高的16位的1,所以就是0;
下图展示的是所有显式数值转换的表。
转换的时候还需要注意符号上的问题,有符号的数据类型最高位的1用来表达负数,而转换为无符号数值的时候,最高位的符号位的1 会被当成数值。
有些数据类型转换不能使用 Cast 形式转换,就需要借助工具类完成转换。
Convert 工具类:几乎可以把任何一种数据类型转化为你要的数据类型;
Parse 方法只能解析格式正确的字符串类型,如果输入的字符串不符合格式,则会报错;
double x = double.Parse(Console.ReadLine());
int y = int.Parse(Console.ReadLine());
也提供了另外一个方法也就是tryParse,符合类型返回 Ture;
显式类型转换操作符背后的工作原理如上图,隐式类型转换则是下图
最终的输出结果都是 10。
位移操作符 >> <<
位移操作符,指的是数据在内存中的二进制数据向左或向右一定数量的位移,示例如下:7的二进制为111,向左移位的时候可以看到,当在没有溢出的情况下,向左移一位相当于乘以二,如果造成溢出,在unchecked的上下文环境中,会导致数据的溢出但不会报错,如果在checked的上下文中,会收到一个Overflow的异常。
下图为右移,同样是在unchecked的上下文环境中,不会报错,但会溢出,这样数据就不正确了。
如果是一个负数,向右移动最高位应该补什么数:
左移不论正数还是负数数,最高位补0;右移的时候,如果正数最高位补0,如果是负数最高位补1
所有关系操作符的运算结果,要么就是真,要么就是假;
类型检验操作符 is as
上图可以看到,通过is判断数据类型,如果这个类是另外一个类派生而来的,那么也可以判断前者的类型为后者的父类的类型。
as的用法类似,如果 A 是 B的数据类型,那么就将A的地址交给C,否则交null值给C;
这里需要注意,这里的 as 的用法和 python 中 as 的用法不一致,python转过来的朋友要注意!!!
可空类型
int? x = null;
Nullable<int> y = null;
条件操作符 ?:
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){int x = 80;string str = string.Empty;if (x >= 60){str = "Pass";}else {str = "Failed"; }Console.WriteLine(str);}}
}
可以看到 if else 分支占了大部分空间,使用下面的方法可以实现相同的方法。
namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){int x = 80;string str = string.Empty;str = (x >= 60) ? "Pass" : "Failed";Console.WriteLine(str);}}
}