C#编程学习笔记(一)
C#编程学习笔记(一)
C#完全面向对象,一个项目也是一个类,一个功能也可以是一个类
名称空间(namespace):类似python中的包,但是也可以不使用using,直接使用名称空间
using:引入名称空间,类似python中的import,方便管理类,也防止类的名称相同引起的冲突
ctrl+. 查看使用名称空间
dll:类库
dll引用:黑盒引用,看不到源码
项目引用:白盒引用
nuget : 引用网上的类库
一个project可以被多个solution所引用
添加project:在vs中右击solution,Add,Existing Project(现有项)
类(或对象)之间的耦合关系。追求高内聚,低耦合
UML(通用建模语言) 类图:展现类和类之间的关系
Program类依赖于Console类。
从现实世界抽象到类的过程叫建模
编程唯物主义
可以定义类变量,之后再实例化,比如 类 变量名;
com组件是非托管对象,可以不需要.net框架而直接运行,.net框架组件是托管对象,必须有.net框架的支撑才能运行
暂时理解:C#中的引用是浅拷贝,传递的是引用,修改引用后的值会影响原来的值。
.NET Framework和.NET Core
类的三大成员:属性,方法,事件。
事件(event):类或对象通知其他类或对象的机制,为C#所特有,善用事件机制非常重要
在visual studio中,点击一下某个类,按F1就能进入这个类的定义
特殊类或对象在成员方面侧重点:
- 模型类或对象重在属性,如Entity Framework
- 工具类或对象重在方法,如Math,Console
- 通知类或对象重在事件,如各种Timer
静态(Static)成员在语义上表示它是“类的成员”
实例(非静态)成员在语义上表示它是“对象的成员”
绑定(Binging)指的是编译器如何把一个成员与类或对象关联起来(晚绑定:动态语言,早绑定)
. 操作符——成员访问操作符
构成C#语言的基本元素
精通:了解常用的元素的运行机制
- 关键字(Keyword):学习的时候按照逻辑分组来学
- 操作符(Operator)
- 标识符(Identifier)
- 什么是合法的标识符:下划线或字符开头,非要用关键字可以在前面加上@
- 大小写规范
- 命名规范:变量名用小驼峰法,类、方法名、命名空间用大驼峰法,方法名应该是一个动词或者动词短语
- 标点符号
- 文本(字面值)
- 整数:长整型后面加个L(64位 long,短整型32位 int)
- 实数(小数):float单精度(32位后缀加F),double双精度(64位,后缀加D)
- 字符: char,用单引号
- 字符串:string,用双引号
- 布尔:true,false
- 空(null)
- 注释://, /* */
上面的五种叫标记(Token):对于编译器有意义的字符
类型(Type):也叫数据类型
- ```
Type myType = type(Forms);
PropertyInfo[] plnfos = myType.GetProperties(); //查看所有的属性
MethodInfo[] mlnfos = myType.GetMethods(); //查看所有的方法
Console.WriteLine(myType.BaseType.FullName); //查看父类名称class Calculator1
2
3
4
5
6
7
8
9
10
- 变量
- 变量的声明:var声明编译器会自动判断出来类型,一般都使用int,double等明确的标识数据类型
- 变量的使用
- 方法:处理数据的逻辑,又称“算法”
- 方法的声明
{
}public int Add(int a, int b) // 加入public,可以在类外面也能访问这个方法,默认是private,只能在类内访问 { int result = a + b; return result; }
1
2
3
4
5
6
7
- 方法的调用
- ```
Calculator c = new Calculator();
int x = c.Add(2, 3);
Console.WriteLine(x);
- ```
程序 = 数据 + 算法
强类型语言与弱类型语言的:数据受到数据类型的约束,就是强类型语言(java,C#),弱类型:js
- C#为了学习弱类型语言,引入了dynamic关键字,可以不通过强制类型转换就可以进行数据赋值
内存分配
- Stack(栈)简介:给函数调用使用的,比较小
- Stack overflow:栈溢出,死循环会造成栈溢出
- Heap(堆)简介:存储对象,比较大
- 使用Performance Monitor查看进程的堆内存使用量,WIN+R运行perfmon, 选择Process(进程),Private Bytes(不与其他处理共享的,已分配的当前字节数)
- 关于内存泄漏:指堆分配内存后没有回收
- 内存是以字节为单位,每个字节有自己的地址号
C#的类型系统
五大数据类型
类(Classes): 如object, string, Windows,Form,Console
结构体(Structures): 如bool, byte. Int32, Int64,Single
枚举(Enumerations):enum定义的类型,只能从固定的几种定义中选取一种
接口(Interfaces)
委托(Delegates)
C#类型的派生谱系
列表定义:List
变量、对象和内存
变量表示了存储位置,变量名对应着变量的值在内存中的存储位置
变量有七种:静态变量(属于类)、实例变量(成员变量、字段,默认是实例变量),数组元素,值参数,引用参数(ref定义),输出形参(out定义),局部变量
值类型的变量
byte/sbyte/short/ushort:
- byte一个字节(0到255)
- sbyte带符号的一个字节(-128到+127,计算机中负数是以补码存储的,反码:正数反码是本身,负数反码是除符号位的其他位0变1,1变0,补码:正数是本身,负数的补码是:符号位为1,其余各位求反,末位加1)
- ushort:无符号短整型(0~65536),两个字节,高八位(左八位)存在高地址(大一点的地址)
- short:有符号短整型(-32768~32767)
值类型没有实例, 所谓的“实例”与变量合二为一。即int x; 而不是Int x =new Int();
引用类型的变量与实例
引用类型定义的时候给引用类型分四个字节(栈内存,用于存堆内存的首地址,刚开始全部置0),实例化的时候会根据类中的值类型再次分配相应的大小(分配到堆内存),然后将栈内存中的的四个字节改成堆内存的首地址,传递实例也是传递的指针。
局部变量:分配在栈内存
默认值:引用类型中的成员变量在堆内存中分配好之后就会被置0,本地变量不允许未赋值就打印(C#不允许)。
常量:const修饰
装箱与拆箱(Boxing & Unboxing):
装箱:当引用类型发现引用的值不在堆内存中,而是一个栈内存中的值的时候,会把栈内存中的值先copy到堆内存中,然后在写入这个堆内存中的内存地址—–把栈上的值类型的值封装成object类型的实例在堆上
1
2
3int x = 100; // 存在栈内存
object obj; // 存在栈内存
obj = x; // 从x所在的栈内存复制值到堆内存,再存入堆内存的地址拆箱: 把堆上面object类型上实例的值按照要求拆成目标数据类型,然后存到栈上去
1
2// 上接拆箱
int y = (int)obj;现在用的少了,有性能损失。
对象内存分配遵循以下两条原则:
引用类型总是分配到堆内存。
值类型和指针的分配与它们声明位置有关系
方法的定义、调用和调试
方法的由来
- 方法(method)的前身是C/C++语言的函数(function)
- 永远都是类(或结构体)的成员,纯面向对象语言,不能有方法独立于类之外
- C#语言中函数不可能独立于类之外
- 只有作为类(结构体)的成员时才被称为方法
- C++中可以,称为全局函数
- 是类的最基本成员之一(另一个是字段–变量)
- 为什么需要方法和函数
- 隐藏复杂的逻辑
- 把大算法分解为小算法
- 复用(reuse)
方法的声明和调用
method-declaration(声明)
method-header method-body
method-header(只列出了常用的)
method-modifiersopt(opt表示非必须) return-type member-name(动词或动词短语) type-parameter-listopt(formal-parameter-listopt即形参)
method-modifiers:
method-modifier
method-modifiers method-modifier
method-modifier:
new
public
protected
static
…
1 | public static double xxx() // 公共方法,可在外部调用,静态方法,由类使用 |
构造器
构造器(constructor)是类的成员之一
狭义的构造器指的是“实例构造器”(instance constructor)
- 创建实例的时候就是调用了类的构造器,类的构造器会帮助初始化字段。(类似于python中的__init__),并允许多个构造器,默认会置0,整型就是0,字符串全是0的话就是null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 自定义构造器
class Student
{
public Student(int initId, string initName) // 构造器
{
this.ID = initId;
this.Name = initName;
}
public Student() // 另一个构造器,会根据传入的参数进行自动选择
{
this.ID = 1;
this.Name = "No name"; //string,引用类型,存引用,去堆中创建
}
public int ID;
public string Name;
}
构造器的内存原理
- 如果是带参数的构造器,会先在栈内存中分配四个字节,然后根据参数类型个数分配相应的堆内存,如果参数中有string,则会再开辟一块内存存值,之前分配的存地址
方法的重载(Overload)
调用重载方法
声明带有重载的方法
- 方法签名(method signature)由方法的名称、类型形参的个数和他的每一个形参(按从左往右的顺序)的类型和种类(值、引用ref或输出out)组成。方法签名不包含返回类型。方法的名字可以一样,签名不能一样。比如Console.WriteLine()有19个签名,按上下键可以切换,一般情况根据传入的参数自动选择。
1
2
3
4
5
6
7
8
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
- 重载决策(到底调用哪一个重载):用于给定了参数列表和一组后选函数成员的情况下,选择一个最佳函数成员来实施调用。
如何对方法进程debug
- 设置断点(breakpoint)
- 观察方法调用时的call stack:查看调用情况
- Step-in(跟踪到被调函数里边去,也叫step-into,F11),Step-over(不进入子函数,F10), Step-out(退出当前被调函数)
- 观察局部变量的值与变化
方法的调用与栈
- 方法调用时栈内存的分配(主调用与被调用)
- 对stack frame(一个方法在被调用的时候在内存中的布局)的分析:谁调用,谁负责压栈(不同的语言情况不同)
- 未细看
- 方法调用时栈内存的分配(主调用与被调用)
操作符
优先级从上往下一次降低
- 操作符不能脱离与它相关联的数据类型,整数的除号和浮点数的符号就不同
- 操作符的本质是函数(即算法)的“简记法”,比如想使用加号,只需要把方法名改成关键字operator +
- 带有赋值功能的运算符的顺序是由右向左,比如x += y += z,先算y+=z, 再算x +=
点操作符:成员访问操作符,访问外层名称空间中的子名称空间,访问名称空间中的类型,访问类型的静态成员,访问对象的成员
[ ]操作符:元素访问操作符,访问集合(数组和字典)中的元素,数组:int[ ] myIntArrat = new int[ ] {1,2,3}; myIntArray[0]; 字典:Dictionary<string, Student> stuDic = new Dictionary<string, Student>();
后置++操作符:遇到赋值操作符时, y = x++ 先把x的值传递给y,然后再计算x++
typeof操作符:查看一个类型的内部结构(Metadata,元数据),Type t = typeof(int); 可以查看t.Namespace,t.GetMethods[]
default操作符:帮助获取一个类型的默认值,int x = default(int), 结构体类型默认是0,引用类型的默认值是null,枚举类型(enum定义)中返回的是枚举类型中为0的,如果枚举类型没赋值,那就是第一个值,如果没有为0的,那就直接返回0,而非枚举类型中的值。内存块中其实都是0,只是根据不同的类型返回不同的值
new操作符:在内存中创建一个类型的实例,并立刻调用这个实例的实例构造器,还可以调用这个实例的初始化器
1
Form myForm = new Form() { Text = "Hello"}; // 花括号中的就是初始化器
- 显示变量:明确说明是什么类型,比如Int
- 隐式变量:用var定义,由赋值的时候编译器去判断是什么类型
- 强类型语言,一旦确认了类型就不能再修改了
- string类型是类,但是不用new操作符,这是个语法糖,其实底层用了
sizeof操作符:在内存中占用的字节 ,也可以获取自定义的类型大小,但是要在unsafe中执行
-> 操作符:指针访问操作符,C#中的指针要在unsafe中使用
一元操作符:只有一个操作数
&x:取地址操作符,需要不安全的上下文,取数据在内存中的地址
Student* pStu = &stu;
*x:引用操作符,需要不安全的上下文,拿到内存中地址所指的对象
1
(*pStu).Score = 100; // 加括号是因为点操作符优先级高于引用操作符
+ - 操作符:正号和负号,通过一个操作数来区分加减号
前置++操作符:单独使用时和后置++相同,遇到赋值操作符时,会先算++,然后再赋值
(T)x:强制类型转换操作符
隐式类型转换:由编译器区自动判断类型进行转换
- 不丢失精度的转换:比如int型向long型转换,小范围向大范围数值转换
- 子类向父类的转换:子类向父类传递时,只会看到父类中的方法
- 装箱:值类型的实例从栈转移到堆里去
显示类型转换
有可能丢失精度(甚至发生错误)的转换,即cast(铸造,高精度向低精度转换):(T)x,T表示要转到的类型
拆箱
使用Conver:进行类型转换的工具类,一般情况下string不能向int类型转换,差别太大,就可以使用Conver
ToString方法与各数据类型的Parse/TryParse方法:Parse只能解析可以转换的数值,不符合格式的话直接报错,TryParse可以返回一个bool类型,告知是否可以转换
1
double x = double.Parse("12.0");
自定义类型转换操作符
1
public static explicit operator Monkey(Stone stone) // 定义了一个类型转换的方法,在Stone类中定义,explicit:显示类型转换
1
explicit
1
{ Monkey m = new Monkey(); m.Age = stone.Age / 500; return m; }
<< : 左移操作符,没有溢出的情况下,左移一位就是乘二,右移一位就是除二
is操作符:判断是否是一个类,继承下来的也算是同一个,类都是从object类型派生出来的, 但是派生出来的类和以前的不是同一个,子类属于父类,父类不属于子类
as操作符:如果是一个类,就把这个类的引用传递过去进行强制类型转换,如果不是,则传null
?:可空类型修饰符,引用类型可以使用空引用表示一个不存在的值,而值类型通常不能表示为空,比如int x = null会报错,这时候可以使用可空操作符 int? x = null;
??操作符
1
int y = x ?? 1; // 如果x为null,则y赋值1,不为null,则是x本身
?: 操作符
1
string str = x > 10 ? "Pass" : "Failed";
快捷键:
for 然后连敲两下tab,会出来for循环的结构
ctor,然后一下tab,自动构建一个构造器框架
使用unsafe:菜单栏PROJECT—>OperatorsExample Properties —> Build —> 勾选Allow unsafe code