C# 学习笔记
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 b
public double Add(double a, double b)
{
// 局部变量 例如 result 声明在 Add 函数体当中,
// 那么 result 就是函数体 Add 的局部变量
double result = a + b;
return result;
}
// 引用参数变量,在参数前加上 ref,例如: ref double a
public double Add(ref double a, double b)
{
return a + b;
}
// 输出参数变量,在参数前加上 out,例如: out double a
public 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);
}
}
}
Section 7 表达式、语句详解(待补充 - 20230823)
语句的内容非常多,非常重要,因为构成了程序的主体;
Part 1 各类表达式概览
Part 2 语句 (待补充)
Part 3 语句详解
- statement
-
- labeled-statement 标签语句
-
- declaration-statement 声明语句
-
- embedded-statement 嵌入式语句
什么是嵌入语句,先来看最常见的选择语句,也就是判断语句,if语句就是最典型的选择语句,以此为例介绍什么是嵌入语句
- embedded-statement 嵌入式语句
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
bool result = 5 > 3;
if (result)
Console.WriteLine("Hello World"); // 嵌套在其他语句中的语句就是嵌入式语句
}
}
}
可以看到,蓝色选中的部分,是一条选择语句,它嵌套在上一条选择语句当中,组成了一个复合语句。
这里没有加花括号是因为单条语句可以不加花括号看,花括号是块语句
可以看到右侧,横线向上的是即使初学者也需要掌握的,横线下面的使比较进阶的;
1 ) 声明语句 Declaration-statement
- 局部变量声明
int x;
x = 100;
// 和下面
int x = 100;
// 有什么区别?
// 二者不是一回事儿,前者叫做声明变量的时候没有初始化,而在后面进行了赋值,是两步操作;
// 后者叫做声明变量的时候追加了初始化器,是一步操作;
- 常量声明
常量:在声明并初始化之后,值不能再改变的量;
int x = 100; // 值是可以改变的
const int y = 100; // 值在上下文是不可以改变的 const = constant
// const 常量必须在初始化的时候跟上初始化器,如果删掉的话,是编译不过去的;
2 ) 表达式语句 Expression-statement
可以形成语句的表达式;
Console.WriteLine("Hello"); // 方法调用表达式
new Form(); // 对象创建表达式,也就是new操作符创建的表达式
x = 100; // 赋值语句
++x;
--x;
x++;
x--;
// await 表达式,异步编程使用
由此表达式计算出来的值(如果有)被丢弃
3 ) 块语句 Block
块语句相当于语句的容器,块语句是一条语句,这个语句内可以容纳多个语句,可以理解为 if 后面的花括号,那个花括号就是块语句;
花括号是空的,就是空的块语句,这个用法比较少;
上图中的花括号,并不是块语句;最外层的是名称空间体,第二层是类体,第三层的是Main的方法体;
只有像这样出现在方法体中的花括号,才是块语句,图中展示的是空的块语句;块语句后面不需要加分号;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
int y = 200;
{
int x = 100;
if (x > 80)
Console.WriteLine(x);
hello: Console.WriteLine("Hello World"); // 标签语句
goto hello;
}
}
}
}
我们可以看到,块语句中包含了许多的子语句,但是编译器始终把这个块语句当成一个语句;
上述的例子在块语句中,可以看到在块之外声明的 y;而在块语句外,是看不到块语句内声明的 x;
4 ) 选择(判断、分支)语句 Selection-statement
if 语句
switch 语句
Switch 语句示例:
int score = 95;
switch(score/10) // Switch 后面的表达式类型必须和case后面的表达式的类型一致;
{
case 10: // case 后面的表达式必须是个常量表达式,变量则会编译报错
if(score == 100)
{
goto case 8;
}
else
{
goto default;
}
case 8:
case 9:
Console.WriteLine("A");
break; // 必须加上 break
case 6:
case 7:
Console.WriteLine("B");
break;
case 4:
case 5:
Console.WriteLine("C");
break;
case 0:
case 1:
case 2:
case 3:
Console.WriteLine("D");
break;
default: // 类似if语句中的else,不符合前面所有条件,执行default的selection
Console.WriteLine("Error!");
break;
}
try 语句 --------------------
- try 语句可以使用 catch 子句捕获异常再分门别类进行处理;
- try 语句还可以再带一个 finally 语句,也就是无论语句发生了什么或者没发生什么,都要执行 finally 里面的子句;
- 对于 try 语句而言,可以有多个 catch 语句但只能有一个 finally 语句,当出现多个 catch 子句的时候,只能执行其中的一个 catch 子句,不能执行多个 catch 子句;
- 对于 catch 子句,有两类,一个是通用的,一个是只能捕获某一种异常;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
int r = 0;
try
{
r = calculator.Add("123", "200");
}
catch (OverflowException oe)
{
// Add 没有处理的异常,交由调用者处理
Console.WriteLine(oe.Message);
}
Console.WriteLine(r);
}
}
class Calculator
{
public int Add(string arg1, string arg2)
{
int a = 0; // 提前声明
int b = 0;
bool hasError = false;
try
{
a = int.Parse(arg1);
b = int.Parse(arg2);
}
catch (ArgumentNullException ane) // 精细捕捉错误
{
Console.WriteLine("Your argument(s) are null");
Console.WriteLine(ane.Message); // 通过标识符捕获异常的详细信息
hasError = true;
}
catch (FormatException fe)
{
Console.WriteLine("Your argument(s) ar not number");
Console.WriteLine(fe.Message);
hasError = true;
}
catch (OverflowException oe)
{
// Console.WriteLine("Out fo range!");
// Console.WriteLine(oe.Message);
// hasError = true;
throw oe; // 直接抛出异常,不在 Add 方法里处理
// 谁调用谁处理
// throw 关键字的语法比较灵活,后面没有标识符也是可以抛出的
// 在 catch 中没有标识符时,也是可以抛出的
}
finally
{
// 通常 finally 子句包含了两类内容
// 释放系统资源的语句,无论try语句是否异常,资源都会释放
// 写程序的执行记录 Log
if (hasError)
{
Console.WriteLine("Execution has error!");
}
else
{
Console.WriteLine("Done!");
}
}
int result = checked(a + b);
return result;
}
}
}
当发现某个语句可能产生异常的时候,就要使用 try 语句来执行逻辑;
程序崩溃的Bug是所有的Bug中最严重的一个;
作为开发人员,需要尽可能多地处理异常,不然年终 review 很难看、工资不保;
5 ) 迭代语句(循环语句)iteration-statement
- while-statement
- do-statement
- for-statement
- foreach-statement
while语句
当满足某个条件的情况下,反复地执行循环体,也可能循环体不执行;
while 语句按不同条件执行一个嵌入语句零次或多次
while(booleam-expression)
{
embedded-statement
}
// 例子
int score = 0;
bool canContinue = true;
while (canContinue )
{
Console.WriteLine("Please input first number");
string str1 = Console.ReadLine();
int x = int.Parse(str1);
Console.WriteLine("Please input second number");
string str2 = Console.ReadLine();
int y = int.Parse(str2);
int sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct! {0}+{1}={2}", x, y, sum);
}
else
{
Console.WriteLine("Error! {0}+{1}={2}", x, y, sum);
canContinue = false; // 当canContinue为false的时候,while的循环体就不再执行了
}
}
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER");
do语句
do语句按不同条件执行一个嵌入语句一次或多次;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
int score = 0;
int sum = 0;
do
{
Console.WriteLine("Please input first number");
string str1 = Console.ReadLine();
int x = int.Parse(str1);
Console.WriteLine("Please input second number");
string str2 = Console.ReadLine();
int y = int.Parse(str2);
sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct! {0}+{1}={2}", x, y, sum);
}
else
{
Console.WriteLine("Error! {0}+{1}={2}", x, y, sum);
}
}
while (sum==100);
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER");
}
}
}
for语句
for 语句就是专门为计数循环而设计的,因此在计数条件的可读性上要优于while和do的;
- for-initializer for循环的初始化器;
- for-condition for循环可以执行的条件;
- for-iterator for每次执行完循环体之后,都会执行一次这个iterator;
for语句开始执行的时候,初始化器执行且只执行一次;
然后执行condition判断for循环体是否得到执行,当true,执行循环体;
可以看到,都是每次执行完循环体之后,counter才 +1;
平时非必要不要把循环变量写到for的外面;
如果 for 的初始化器,执行条件和iterator都不写也就是 for( ; ; )
这种写法,等效于while True,无限循环;
9 by 9乘法表Demo:
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
for (int a = 1; a <= 9; a++)
{
for (int b = 1; b <= a; b++)
{
Console.Write("{0}x{1}={2}\t", b, a, a * b); // \t 是制表符
}
Console.WriteLine();
}
}
}
}
foreach语句
常说的集合便利循环,也就是常说的遍历这个集合;
foreach语句用于枚举一个集合的元素,并对该集合中的每个元素执行一次相关的嵌入语句;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
int[] intArray = new int[] { 1, 2, 3, 4, 5, 6};
Console.WriteLine(intArray.GetType().FullName);
Console.WriteLine(intArray is Array);
IEnumerator enumerator = intArray.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
enumerator.Reset(); // 重置枚举器,否则下面的while不执行,因为上面执行完之后已经指向数组的末尾,需要重新指向集合的头
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
}
上述的例子讲述了集合遍历的底层原理和迭代器;
foreach就是对集合遍历的简记法;
local-variable-type: 本地数据类型
identifier:迭代变量,相当于迭代器把集合中的元素从头到尾全都指一遍,
expression 也就是集合,这个表达式最终的结果是一个集合;
拿到迭代器,拿到集合就可以一个一个访问集合中的元素,每访问一个元素,就执行一次循环体;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
int[] intArray = new int[] { 1, 2, 3, 4, 5, 6};
Console.WriteLine(intArray.GetType().FullName);
Console.WriteLine(intArray is Array);
foreach (var current in intArray) // 这里鼓励大家保留 var 定义
{
Console.WriteLine(current);
}
}
}
}
foreach的最佳应用场合就是对集合进行遍历!!!
foreach的最佳应用场合就是对集合进行遍历!!!
6 ) 跳转语句 Junp-statement
continue 和 break 二者与循环语句结合紧密
continue语句
放弃当前这次循环,开启一次新的循环;实例如下:
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
int score = 0;
int sum = 0;
do
{
Console.WriteLine("Please input first number");
string str1 = Console.ReadLine();
int x = 0;
try
{
x = int.Parse(str1);
}
catch
{
Console.WriteLine("First Number Has Problem! Restart");
continue; // 放弃本次循环,开启下一次循环
}
Console.WriteLine("Please input second number");
string str2 = Console.ReadLine();
int y = 0;
try
{
y = int.Parse(str2);
}
catch
{
Console.WriteLine("Second Number Has Problem! Restart");
continue; // 放弃本次循环,开启下一次循环
}
sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct! {0}+{1}={2}", x, y, sum);
}
else
{
Console.WriteLine("Error! {0}+{1}={2}", x, y, sum);
}
}
while (sum==100);
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER");
}
}
}
可以看到检测到输入内容有误时,重新开启了一轮循环
break语句
立刻结束这次循环,不再循环;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
int score = 0;
int sum = 0;
do
{
Console.WriteLine("Please input first number");
string str1 = Console.ReadLine();
if (str1.ToLower() == "end")
{
break; // 结束当前循环,不再开启新的循环
}
int x = 0;
try
{
x = int.Parse(str1);
}
catch
{
Console.WriteLine("First Number Has Problem! Restart");
continue; // 放弃本次循环,开启下一次循环
}
Console.WriteLine("Please input second number");
string str2 = Console.ReadLine();
if (str2.ToLower() == "end")
{
break; // 结束当前循环,不再开启新的循环
}
int y = 0;
try
{
y = int.Parse(str2);
}
catch
{
Console.WriteLine("Second Number Has Problem! Restart");
continue; // 放弃本次循环,开启下一次循环
}
sum = x + y;
if (sum == 100)
{
score++;
Console.WriteLine("Correct! {0}+{1}={2}", x, y, sum);
}
else
{
Console.WriteLine("Error! {0}+{1}={2}", x, y, sum);
}
}
while (sum==100);
Console.WriteLine("Your score is {0}.", score);
Console.WriteLine("GAME OVER");
}
}
}
输入break之后,可以看到程序退出了;
需要注意的是,continue 和 break 只作用于直接包含这个语句的循环,不作用于间接包含这个语句的循环;
return语句
尽早 return 原则,下面是例子:
下面的图不是尽早return;
下面的图是尽早return:
尽早return的,这样写的好处,在于可以让读代码的人,快速鉴别出参数在什么情况下是有问题的,而且避免整个方法写出来头重脚轻,让整个方法结构清晰;
如果方法的返回值不是void且在方法体中使用了选择语句,必须要保证在每一个选择分支中都可以让这个方法return
下面是例子:
如果不保证每个分支都能让这个方法return,则程序会出错;
Section 8 字段、属性、索引器、常量
标题中四种成员,都是用来表达数据的;
一个程序的本质,就是数据+算法,而这四个都是用来表达数据的,所以一起讲;
C# 类型具有以下这些成员:
上图的类型,指的是一个类型内部嵌套的子类型;
Part 1 字段 field
什么是字段:
- 字段 field 是一种表示与对象或类型(类与结构体)关联的变量,是用来存储数据的,多个字段组合起来可以表达一个对象或一个类型当前的状态;
- 字段是类型的成员,旧称成员变量;
- 与对象关联的字段亦称为实例字段,帮助实例或对象保存数据,隶属于某个实例或对象帮助对象保存当前状态;
- 与类型关联的字段称为“静态字段”,由 static 修饰,隶属于某个数据类型;
上图展示的是,学生这个数据类型的状态的状态是:学生不多,只有两个,这个就是通过静态字段表达类型当前的状态。通过下面的例子展示了实例字段和静态字段的功能;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
List<Student> studentsList = new List<Student>();
for (int i = 0; i < 100; i++)
{
Student student = new Student();
student.Age = 24 + i;
student.Score = i;
studentsList.Add(student);
}
int totalAge = 0;
int totalScore = 0;
foreach (var student in studentsList)
{
totalAge += student.Age;
totalScore += student.Score;
}
Student.AverageAge = totalAge / Student.Amount;
Student.AverageScore = totalScore / Student.Amount;
Student.ReportAmount();
Student.ReportAverageAge();
Student.ReportAverageScore();
}
}
class Student
{
public int Age;
public int Score;
public static int AverageAge;
public static int AverageScore;
public static int Amount;
public Student()
{
Student.Amount++;
}
public static void ReportAmount()
{
Console.WriteLine(Student.Amount);
}
public static void ReportAverageAge()
{
Console.WriteLine(Student.AverageAge);
}
public static void ReportAverageScore()
{
Console.WriteLine(Student.AverageScore);
}
}
}
字段的声明:首先字段的名字必须是一个名词;
字段是类型的成员,当为一个类声明字段,必须在类体里面,如果写到函数体里面就是局部变量了;
最常用的字段声明方法;
public int Age;
访问级别,数据类型,变量名
public static int Amount;
访问级别,static,数据类型,变量名;
在声明的时候进行赋值的行为,和在其构造器中进行初始化是一样的;
初始化的时机:
- 对于实例字段,在实例创建的时候,每次创建实例的时候,都能执行;
- 对于静态字段,在运行环境加载数据类型的时候,只有第一次加载数据类型的时候才执行,也就是静态构造器永远只执行一次;
无显式初始化时,字段获得其类型的默认值,所有字段“永远都不会未被初始化”;
readonly 只读字段是不能被赋值的,只能对该字段初始化,不能赋值,功能是为实例提供一旦初始化后就不希望再改变的值;
Part 2 属性 Property
- 属性是一种用于访问对象或类型的特征的成员,特征反映了状态;
通过下面的例子理解上面这句话 :
有一块豆腐,这个豆腐有一个属性或者是特征温度,这个值非常高,反映了豆腐非常烫的状态;
如果这个属性特别低,说明豆腐是冻起来的状态;
- 属性是字段的自然扩展:
-
- 从命名上来看,field更偏向于实例在内存中的布局,而Property更偏向反映现实世界对象的特征;
-
- 对外,暴露数据,数据是可以储存在字段里的,也是可以动态计算出来的;
-
- 对内,保护字段不被非法值“污染”,如果只使用字段,则容易被污染;
- 属性由Get/Set方法对进化而来
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
Student student1 = new Student();
student1.SetAge(20);
Student student2 = new Student();
student2.SetAge(20);
Student student3 = new Student();
student3.SetAge(20);
int avgAge = (student1.GetAge() + student2.GetAge() + student3.GetAge()) / 3;
Console.WriteLine(avgAge);
}
}
class Student
{
private int age;
public int GetAge()
{
return this.age;
}
public void SetAge(int value)
{
if (value >= 0 && value <= 120)
{
this.age = value;
}
else
{
throw new Exception("Age value has error");
}
}
}
}
类似 Java 中的 Get 和 Set 方法,可以在 Set 方法中保护字段不被非法值污染;
下面展示的是使用属性的方法及包装器来保护字段不被非法值污染;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
Student student1 = new Student();
student1.Age = 20;
Student student2 = new Student();
student2.Age = 20;
Student student3 = new Student();
student3.Age = 20;
int avgAge = (student1.Age + student2.Age + student3.Age) / 3;
Console.WriteLine(avgAge);
}
}
class Student
{
private int age; // 私有的字段
public int Age // 公有的属性,是上面那个字段的包装器
{
// 现在就有了 Age 这个属性,这个就是 Age 属性的包装器
get
{
return this.age;
}
set
{
// value 是默认的,不能变,且在特定环境下是上下文关键字;
// 当在写 Setter 访问器,微软将其设定为关键字,代表传进来的,用户设定的值,出了Setter之后就不再是关键字了;
if (value >= 0 && value <= 120)
{
this.age = value;
}
else
{
throw new Exception("Age value has error");
}
}
}
}
}
上面的例子展示的是字段如何一步一步通过 Get Set 方法对演化为属性的过程;
- 属性实际是一个“语法糖”;
Part 3 索引器 Indexer
索引器是这样一种成员:它使对象能够用与数组相同的方式(即使用下标)进行索引;
一般来讲,拥有索引器类型都是集合类型;
用非集合类型讲解,能够凸显索引器的特点:
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
Student student = new Student();
var mathScore = student["Math"];
Console.WriteLine(mathScore); // 还没有数学成绩,打印不出任何东西
student["Math"] = 90;
mathScore = student["Math"];
Console.WriteLine(mathScore); // 赋值之后,可以看到成绩
}
}
class Student
{
private Dictionary<string, int> scoreDictionary = new Dictionary<string, int>();
public int? this[string subject] // int? 是可空类型的int类型
{
get {
/* return the specified index here */
if (this.scoreDictionary.ContainsKey(subject)) // 先索引有没有这个key
{
return this.scoreDictionary[subject]; // 有
}
else
{
return null; // 没有
}
}
set {
if (value.HasValue == false) // 如果 value 是一个空值,使用 throw 扔出一个异常
{
throw new Exception("Score cannot be null");
}
/* set the specified index to value here */
if (this.scoreDictionary.ContainsKey(subject))
{
// 如果存在,更新这个值
this.scoreDictionary[subject] = value.Value; // Value 代表传进来的值,对于可空类型,value.Value 才是真正的值
}
else
{
// 如果不存在,添加这个值
this.scoreDictionary.Add(subject, value.Value);
}
}
}
}
}
像是上面的例子,使用非集合类型的索引器非常少见;
Part 4 常量 const
常量 Constant 表示常量值,在编译器编译代码的时候,用值替换掉常量的标识符以提高效率;
常量隶属于类型而不是对象,即没有实例常量
(实例常量的角色由只读实例字段来担当)
需要注意区分成员常量和局部常量:
各种 只读 的应用场景:
- 为了提高程序的可读性和执行效率–常量;
- 为了防止对象的值被修改–只读字段;
- 向外界暴露不允许修改的数据–只读属性(静态或非静态),功能与常量有一些重叠,使用常量和使用静态只读属性哪个效率高:使用常量的性能高;
- 当希望成为常量的值其类型不能被常量声明接受时(类/自定义结构体)–静态制度字段;
Section 9 传值,输出,引用,数组,具名,可选参数,扩展方法
Part 1 值参数
也叫做传值参数,在声明的时候不带有任何修饰符的参数就是值参数,本质就是作用域在当前方法的局部变量,初始值就是调用方法的时候赋给的实参的值。
值参数相当于新声明的局部变量,或者是传进来的实参的副本,因此在方法体里面对其进行赋值,不会影响到实参的值;
下面的例子是值类型的传值参数的用法:
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
Student student = new Student();
int y = 100;
student.AddOne(y);
Console.WriteLine(y);
}
}
class Student
{
public void AddOne(int x) // 这个 x 是值参数,这个就是值类型的传值参数
{
x = x + 1;
Console.WriteLine(x);
}
}
}
下面的例子是引用类型的传值参数的用法:
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
Student stu = new Student() { Name = "Tim"};
SomeMethod(stu);
Console.WriteLine("{0},{1}", stu.GetHashCode(), stu.Name); // 可以看到这里打印的 HashCode 与 SomeMethod 中的 HashCode 不一样
// GetHashCode() 可以认为是获取实例对象的某个编号,是唯一的,每个实例的编号都不一样;
// 如果方法没有使用副本而是使用本体,那么程序打印出来的 HashCode 应该是一样的,不一样就是产生了新的实例
}
static void SomeMethod(Student stu)
{
stu = new Student() { Name = "Tim" };
Console.WriteLine("{0},{1}", stu.GetHashCode(), stu.Name);
}
}
class Student
{
public string Name { get; set; }
}
}
Part 2 引用参数
在声明的时候使用 ref 修饰符声明的形参,与值形参不同,引用参数并不创建新的储存位置,相反引用形参的储存位置恰是在方法调用中作为实参给出的那个变量所表示的储存位置;
也就是引用参数指向传进来的实际参数的内存地址;(个人认为和指针差不多;);
需要注意的是,在调用引用参数的方法的时候,实际参数必须赋值,并且使用的时候必须把 ref 关键字加上;
传值函数在内存中创建了实际参数的副本,也就是student和outerStu的实际地址不一样,但存储相同的实例在堆内存中的地址;而第一张图使用引用参数 ref 的那个,二者是同一个地址;
Part 3 输出参数
用 out 修饰符声明的形参是输出参数;带有输出参数的方法,可获得除了返回值之外额外的输出;
输出参数在传入方法之前不一定要赋值
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
Student student = null;
bool b = StudentFactory.Create("Tim", 34, out student);
if (b == true)
{
Console.WriteLine("Student {0}, Age is {1}", student.Name, student.Age);
}
}
}
class Student
{
public int Age { get; set; }
public string Name { get; set; }
}
class StudentFactory
{
public static bool Create(string stuName, int stuAge, out Student result)
{
result = null;
if (string.IsNullOrEmpty(stuName))
{
return false;
}
if (stuAge < 20 || stuAge > 80)
{
return false;
}
result = new Student() { Name=stuName, Age=stuAge};
return true;
}
}
}
这个示例是用来讲解引用类型的输出参数;
Part 4 数组参数
数组参数在声明的时候必须使用 params 修饰符修饰;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
int[] myArray = new int[] {1, 2, 3}; // 不需要再这样声明完数组 myArray;
int result = CalculateSum(1, 2, 3);
// 意味着并不需要提前声明一个数组,可以以 params 的形式输入在这,编译器会自动声明数组
Console.WriteLine(result);
}
static int CalculateSum(params int[] intArray)
{
int sum = 0;
foreach (var item in intArray)
{
sum += item;
}
return sum;
}
}
}
在一个参数列表当中,只能有一个 params 参数,且只能为最后一个;
Part 5 具名参数
指的是在调用一个方法,传进去的参数是带有名字的
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
PrintInfo(name: "TIM", age: 34); // 这个就是具名调用
}
static void PrintInfo(string name, int age)
{
Console.WriteLine("Hello {0}, you are {1}", name, age);
}
}
}
有两个优点:
- 提高代码的可读性;
- 加上名字,参数的顺序就不再受到参数列表位置顺序的限制;
Part 6 可选参数
在调用一个方法的时候,这个参数可选可不选,带有默认值;
Part 7 扩展方法(this 参数)
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
double x = 3.14159;
double y = x.Round(4); // x 是 Round 方法的第一个参数,所以只需要输入第二个参数
Console.WriteLine(y);
}
}
static class DoubleExtension
{
public static double Round(this double input, int digits)
{
double result = Math.Round(input, digits);
return result;
}
}
}
当我们无法对一个类型的源码进行修改的时候,可以使用扩展方法对目标数据类型添加方法;
不使用LINQ
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using System.Collections;
using ClassTest;
using System.Dynamic;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
List<int> myList = new List<int>() { 11, 12, 13, 14, 15};
bool result = AllGreaterThanTen(myList);
Console.WriteLine(result);
}
static bool AllGreaterThanTen(List<int> intList)
{
foreach (var item in intList)
{
if (item <= 10)
{
return false;
}
}
return true;
}
}
}
使用 LINQ:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using System.Collections;
using ClassTest;
using System.Dynamic;
using System.Linq;
namespace ConsoleHelloWorld
{
class Program
{
static void Main(string[] args)
{
// 判断 myList 中是不是所有数都大于 10
List<int> myList = new List<int>() { 11, 12, 13, 14, 15};
// All() 是扩展方法
bool result = myList.All(i => i > 10); // All 方法可以接受委托类型的参数
Console.WriteLine(result);
}
}
}