c#基础学习
C#编程入门(Brackeys)
.NET && VSCode
- 安装.NET框架
- 安装VSCode
- VSCode安装C#插件
控制台输入创建新项目命令:
1 | dotnot new |
启动项目命令:
1 | dotnet run |
自定义项目启动控制台:
- Ctrl+Shift+P打开VSCode控制台
- 选择.NET:Generate Assets For Build and Debug
- VSCode自动生成launch.json配制文件
- 修改control属性为externalTerminal
1 | Console.WriteLine("Hello, World!"); |
How to Program in C#
在Program.cs中,体验一下C#语法的手感:
- Console.ForegroundColor 设置输出颜色
- Console.Title 设置窗口标题
- Console.WriteLine 输出
- Console.ReadLine 读取用户输入
- Console.ReadKey 读取用户按键
一篇只用Console的Visual Novel
1 | // See https://aka.ms/new-console-template for more information |
《C#语言入门详解》——刘铁猛
1. C#语言介绍
编程学习路径:
纵向: 语言 → 类库 → 框架
横向: 命令行 → 桌面程序 → 设备(平板/手机)程序 → Web程序 → 游戏 → …
主窗口代码:
1 | <Window> |
对Button的Click事件进行监听:
1 | namespace HelloWPF |
2. C#应用程序介绍
- Solution:针对客户需求的解决方案
- 用户需要制作一个管理系统
- Project: 解决具体问题采取的方案
- 数据存储?数据库项目
- 数据读取/表单处理?服务器项目
- 网站操作?客户浏览器应用项目
- 手机操作?手机端应用项目
- 平板操作?平板应用项目
- …
Console
Console App 控制台程序
1 | Console.WriteLine("Hello World"); |
Windows Forms(Old)
WinFormsApp 旧版本桌面应用程序
1 | namespace HelloWinFormsApp{ |
WPF(Windows Presentation Foundation)
WPF 新版桌面应用程序
- ASP.NET Web Forms(Old) 网站应用
- ASP.NET MVC(Model-View-Controller) 网站应用
- Windows Store Application 平板应用
- Windows Phone Application 手机应用
- Cloud(Windows Azure) 云计算平台
- WF(Workflow Foundation)工作流
- WCF 纯网络服务
3. 类与名称空间
- c#中,程序作为类实现,因此需要将入口函数包在类中
- 命名空间
- 方便实现类间的依赖关系
- 防止命名冲突
- DLL(Dynamic Link Library 动态链接库/类库)引用
- 黑盒引用,无源代码
- NuGet,自动化维护类依赖关系
- 项目引用
- 白盒引用,有源代码
下面是一个类库(.NET Framework)的主代码
1 | using System; |
在需要引用该类的主项目所在的Solution中,将类库项目添加进去,引入类库依赖,
之后再主项目中使用该类:
1 | using Tools; |
4. 类、对象、类成员简介
两者的区别主要在语境上:
- 在讨论类在现实世界中的对应时,一般称为对象
- 在讨论类在程序中的对应时,一般称为实例
C#中类的三要素:
- 属性 Property
- 方法 Function
- 事件 Event
Visual Studio中,将光标放在类上,点击F1,会自动跳转到类对应的MSDN文档上
1 | using System.Windows.Threading; |
- 静态成员:类的成员
- 实例成员:对象的成员
5. 构成C#语言的基本元素
1 | int x_int = 2; // 32bit |
除了这些静态类型,
C#中还提供2中比较特殊的类型:
- var 可以为任意基础类型
- dynamic 动态类型
6. 类型、变量与对象
- 堆
- 存储实例
- 分配内存大
- 分配不合理会导致内存泄漏
- C#提供垃圾回收机制,防止内存泄漏
- 栈
- 用于存储运行的函数
- 可能会导致栈溢出
使用Performance Monitor,能够监控进程的堆内存使用量。
Win+R → perfmon → 性能监视器 → +号 → Process → Private Bytes(已分配字节数)→ 选定对象实例(要监控的程序)→ 添加 → 双击实例(进行图表定制)
C#的所有数据类型都以Object为基类
- 引用类型
- 类 class
- 接口 interface
- 委托 delegate
- 值类型
- 结构体 struct
- string、char
- bool、true、false
- byte、int、long、float、double、sbyte、short、uint、ulong、ushort、decimal
- void、null、var、dynamic
- 枚举 enum
- 结构体 struct
- 静态变量 (const修饰符)
- 实例变量(成员变量,字段)(public/static/private修饰符)
- 数组变量
- 值参数
- 引用参数 (ref修饰符)
- 输出形参 (out修饰符)
- 局部变量
7. 运算符
运算符实际上是对函数的一种语法糖,
定义一个Person类的相加方法:
1 | class Person { |
调用加法的效果:
1 | Person man = new Person(); |
点运算符:
1 | System.IO.File.Create("D:\\CS_Demo\\test\\HelloWorld.txt"); |
函数调用符/事件委托
1 | Action sayHello = new Action(Person.SayHello); |
1 | class Person{ |
数组访问
1 | int[] myIntArray = new Int[] {1,2,3,4,5}; |
字典访问
1 | Dictionary<string, Person> PersonDic = new Dictionary<string, Person>(); |
类型type操作符
1 | Type t = typeof(int); |
default操作符:获取到此类型存储内存全部刷成0的值
1 | int a = default(int) |
功能1:创建实例并将实例的地址转交给变量
1 | Form myForm = new Form() |
功能2:实例初始化
1 | Form myForm = new Form(){Text="Hello",FormBorderStyle = FormBorderStyle.SizableToolWindow} |
功能3: 创建匿名类型
1 | var person = new {Name = "Mr.Okay", Age = 20}; |
功能4:子类隐藏父类方法
1 | class Student { |
什么是依赖注入?
依赖注入又是如何对程序进行解耦合的?
用于检查某个值的溢出情况,
C#中默认进行unchecked,
进行变量的checked检查:
1 | uint x = uint.MaxValue; |
进行代码块的checked检查
1 | checked{ |
delegate是声明匿名方法的操作符,
比如:某个函数声明后,实际只使用了一次:
1 | public MainWindow(){ |
这时可以使用delegate来直接声明(现在多被lambda函数取代)
1 | this.myButtonBox.Click += delegate (object sender, RoutedEventArgs e){ |
lambda表达式的方式更加简单,并且程序会对参数类型进行推断
1 | this.myButton.Click += (sender, e) => { |
使用指针时,代码块需要开启unsafe模式,
并且需要进行额外配置:
项目/Oporator属性/生成/允许不安全代码(勾选)
1 | unsafe { |
- 隐式类型转换
- 不会影响数值精度的情况下完成转换(低精度转高精度)
- 子类向父类的转换
- 显式类型转换
- 可能丢失精度的转换,cast转换
- Convert调用
- ToString/Parse/TryParse
1 | internal class Program |
1 | uint x = ushort.MaxValue + 1; |
Parse在无法转换时,会抛出错误
1 | string x = Console.ReadLine(); |
对应TryParse,接收无法转换的值不会报错,需要一个out参数:
1 | string x = Console.ReadLine(); |
通过在类中定义带关键字前缀函数,实现类型转换:
- 显示转换 explicit
1 | Stone stone = new Stone(); |
- 隐式转换 implicit
1 | // 隐式转换 |
浮点数的除数可以是0,结果会是正负无穷大
1 | double x = -5.0; |
比较ASCII编码大小
1 | char char1 = 'A'; |
is实现类的检测
1 | Person p = new Person(); |
as能够实现类型断言(转换),转换不成功则返回null
1 | object p = new Person(); |
可空类型:
1 | Nullable<int> x = null; |
语法糖:
1 | int? x = null; |
null值检测语法糖,下面语句,如果x为null,则返回1
1 | int y = x ?? 1; |
1 | int x = 80; |
8. 表达式语句
Visual Studio对项目经过编译后,
可以在项目目录/bin/Debug/下找到.exe执行文件,
使用Visual Studio提供的工具: Developer Command Prompt
命令行输入:
1 | ildasm |
导入已经被编辑好的exe执行文件,能够反编译出源码:
1 | const int x = 100; |
1 | hello: Console.WriteLine("Hello, World!"); |
快捷键:
- ctrl + } 实现代码块括号跳转
- ctrl + l 快速剪切一行
visual studio创建switch语句快捷键:sw
case表达式后跟随的数据必须要和switch变量后指定的表达式一致,
case表达式后,有执行语句,必须要加break
1 | try |
1 | class Calculator { |
对于实现IEnumerable接口的类,都能够通过foreach进行循环遍历:
IEnumerable类本身实现了一些用于遍历的方法和属性:
- MoveNext 指针移动到下一项,如果成功返回true
- Current 返回当前值
- Reset 重置当前值
使用IEnumerable实现循环:
1 | List<int> intList = new List<int>() { 1, 2, 3 }; |
使用foreach进行遍历:
1 | List<int> intList = new List<int>() { 1, 2, 3 }; |
9. 字段、属性、索引器、常量
实例字段/静态字段
- public
- static
1 | static void Main(string[] args) |
只读字段 readonly
无论是值类型还是引用类型,readonly变量都不能修改:
1 | static void Main(string[] args) |
私有字段 private
1 | static void Main(string[] args) |
get/set 实现属性封装
Visual Studio快捷键:
- propfull + tabtab :快速定义并封装私有变量
- prop + tabtab : 简略声明私有变量
- 点击要封装的字段 + ctrl + r + e :重构/封装字段
1 | static void Main(string[] args) |
索引器 Index
1 | static void Index() |
10. 参数
- 传值参数
- 输出参数
- 引用参数
- 数组参数
- 具名参数
- 可选参数
- 扩展方法(this参数)
传值参数
值参数:声明时不带任何修饰符的参数
参数作为传值的副本
1 | static void ValueParam_1() { |
直接修改参数引用的内存地址(new操作)时,
函数内对参数的修改不会影响被传入值
GetHashCode: 获取引用类型指向的地址
1 | static void ValueParam_2() |
直接对(引用类型)传值参数的属性进行修改,实际上就是对被传入参数进行修改:
1 | static void ValueParam_3() { |
引用参数 ref
引用类型使用ref进行标识
1 | static void RefererParam_1() { |
1 | static void RefererParam_2() { |
输出形参 out
out和ref效果类似,
不同点在于out可以传入null值,并且必须在函数体中进行赋值
double.TryParse方法使用的就是out传参:
1 | static void OutputParam() |
out声明值类型参数:
1 | static void TryOut_1 () { |
out声明引用类型
1 | static void TryOut_2() { |
数组参数 params
使用数组参数声明,会自动将传参收集为数组格式,
数组参数只能存在一个,并且是被声明的最后一个参数
1 | static void ArrayParam() { |
string.Split方法参数声明的方式就是params
1 | static void UseSplit() { |
具名参数
一种允许乱序的传参方式:
1 | static void NameParam() { |
可选参数
可选参数需要赋默认值:
1 | static void SelectableParam() { |
扩展方法 this
使用this标识参数,能够实现扩展方法
对double类型的扩展:
1 | // 3. 必须是静态类 |
Language Integrated Query 语言集成查询
Linq内实现了对Enumerable的扩展
1 | static void UseExtension() { |
11. 委托
Action、Func<>
方法委托就是将方法,用指针进行调用,一般作为参数进行传递:
模板方法
回调方法
委托方法1:Action + Invoke
1 | static void Fun(){} |
- 委托方法2:Func<>泛型声明
1
2
3
4
5
6
7
8static int Add(int x, int y){return x + y;}
static int Minus(int x, int y){return x - y;}
Func<int, int, int> delegateAdd = new Func<int, int, int>(Add);
Func<int, int, int> delegateMinus = new Func<int, int, int>(Add);
delegateAdd.Invoke(10,20);
delegateMinus.Invoke(10,20);
delegate声明
delegate类似于声明一个方法签名,
声明一个有参数和返回值特点的模具
1 | static void DelegateClass() { |
模版方法和回调方法
1 | static void TempFunc() { |
接口声明与实现
1 | interface IProductFactory |
修改原本的委托方法参数为接口参数:
1 | class WrapFactory { |
修改方法调用的传参:
1 | static void Main(string[] args) |
多播委托(multicast)
将多播进行合并,按照合并顺序执行
1 | static void Multicast() { |
实现异步的方法
实现异步的三种方法:
- 隐式异步 BeginInvoke
1 | Action action1 = new Action(Func1); |
- 线程 Thread
1 | Thread thread1 = new Thread(new ThreadStart(Func1)); |
- Task
1 | Task task1 = new Task(new Action(Func1)); |
12. 事件
事件基本概念
事件的本质是假装在委托字段上的一个蒙版
事件模型组成部分:
- 事件拥有者 eventSource
- 事件成员 event
- 事件响应者 eventSubscriber
- 事件处理器 eventHandler
- 事件订阅
挂接事件处理器使用的是语法糖:
1 | eventSource.evnet += eventSubscriber.eventSubscriber; |
触发Timer.Elapsed事件:
1 | static void UseEvent() { |
1 | static void UseEvent() { |
被绑定事件的参数类型和返回值如下:
1 | private void ButtonClick(object sender, EventArgs e){} |
- 语法糖
1 | this.button.Click += this.ButtonClick |
- EventHandler
1 | this.button.Click += new EventHandler(this.ButtonClick) |
- delegate 匿名函数委托
1 | this.button.Click += delegate (object sender, EventArgs e){ |
- lambda表达式
1 | this.button,Click += (sender, e) => { |
实现整体事件流程
下面是一个点餐的事件模型:
- 事件拥有者:Customer客户
- 事件成员:OrderEventArgs点餐
- 事件响应者:Waiter服务员
- 事件处理器:Action送餐
- 事件订阅:为客户的点餐事件指派一个服务员并进行响应
定义点餐事件委托,以及事件参数:
1 | public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); |
定义客户:
1 | public class Customer |
为Customer类声明事件类型属性:
1 | public class Customer |
简化写法:
1 | public class Customer |
定义服务员以及订餐响应方法:
1 | public class Waiter |
为客户定义事件触发方法:
1 | public class Customer{ |
通过事件触发和响应串通整个流程:
1 | Customer customer = new Customer(); |
13. 类
析构函数 destructor
析构函数和construct相对应,在实例被销毁时执行
1 | class Chocolate { |
反射
通过class的类型创建objet或dynamic变量承接
object实现反射:
1 | Type t = typeof(Chocolate); |
dynamic反射
1 | Type t = typeof(Chocolate); |
静态构造函数
VSCode构造函数快捷键:ctor
1 | class Pie |
类的声明
- new
- public
- protected
- internal 内部类,只有同一装配集内成员能够访问
- private
- abstract
- sealed 密封类不可作为父类
- static
Tip: Ctrl + - 跳转到上次光标快捷键
每个项目的编译结果就是Assembly装配集,
Assembly主要分为2类:
- exe 可执行文件
- dll 类库
类继承
C#内所有类的基类都是Object
1 | Type t = typeof(Candy); |
带有sealed关键字声明的类无法被继承,
1 | public sealed NoChildClass{} |
- c#中只能继承自一个基类,但能实现多个基接口
- 子类的访问权限不能超过父类
类继承就是对父类进行横向和纵向的扩展:
- 横向: 类属性和方法的增加
- 纵向:对父类内属性和方法的重写
1 | class Chocolate : Candy |
重写&多态
重写标识符:
- 父类 virtual
- 子类 override
隐藏和重写的区别:
- 方法/属性不加重写标识,算作子类对父类的隐藏
- 隐藏下父子类之间没有版本关系
方法和属性的重写:
1 | // 父类 |
14.接口
抽象类&接口
- 抽象类
- 函数成员并未完全实现的类
- 虚方法成员必须是public标识
- 不能实例化,需要靠派生类来实现抽象方法
- 纯虚方法声明关键字:abstract
- 子方法实现关键字:override
- 接口
- 功能等同于纯抽象类
1 | // 接口 |
松耦合
使用接口,而不是具体类来作为另一个类的成员的类型,
接口类成员使用该类的实现类赋值
减轻两个类之间的耦合关系
接口实现:
1 | public interface ICoffin{ |
接口实现类:
1 | public class Coffee: ICoffin{ |
主动耦合类:
1 | public class Custom{ |
主动耦合类的调用:
1 | Custom custom = new Ciustom(new Coffee()); |
单元测试
解决方案 → 添加新项目 → xUnit测试
松耦合的2种测试方式:
- 创建类
- Moq
方式一:创建类
1 | public class CoffeeTests{ |
方式二:Moq
用NuGet搜索下载引入Moq
1 | public class CoffeeTests{ |