文章目录
- C# 基础篇
- 0 环境部署
- 1 Hello world
- 1.1 两种框架
- 1.2 创建项目的过程
- 1.3 项目组成结构
- 1.3.1 解决方案和项目
- 1.3.2 程序集信息
- 1.3.3 引用
- 1.3.4 配置文件
- 1.3.5 程序入口类
- 1.4 解决方案
- 1.5 Debug 和 Release
- 1.6 CSharp虚拟机初始
- 1.7 .Net Framework混合语言开发
- 1.8 托管代码和非托管代码
- 1.9 公共语言运行时CLR
- 2 编程要素
- 2.1 项目组成
- 2.2 命名空间和类
- 2.3 数据类型
- 2.4 变量
- 2.5 注释
- 2.6 当前阶段常见错误
- 2.6.1 命名空间和类名错误
- 2.6.2 变量错误
- 3 输入输出
- 3.1 Write WriteLine
- 3.2 Read ReadLine
- 3.3 字符串格式化方法
- 4 运算符
- 5 类型转换
- 5.1 值类型之间的转换
- 5.2 值类型与字符串类型之间的转换
- 5.3 万能转换器Convert
- 6 程序控制语句
- 6.1 条件选择 if
- 6.1.1 if 语句
- 6.1.2 if-else语句
- 6.1.3 if-else if -else 语句
- 6.2 混插-关于优先级
- 6.3 三元 运算符
- 6.4 switch
- 6.5 for 循环
- 6.6 while 循环
- 6.7 do...while 循环
- 7 字符串
- 7.1 字符串的常用方法
- 7.2 字符串格式化
- 7.3 空值&空对象
- 7.3.1 空值
- 7.3.2 空对象
- 7.4 高效处理字符串
- 7.5 转义字符
- 7.6 字符串分割与连接
- 8 数组
- 9 常量枚举
- 9.1 常量
- 9.2 枚举
- 面向对象Basic
- 10 类与对象
- 10.1 概念
- 10.2 属性Property
- 10.2.1 创建一个类,并实例化
- 10.2.2 自动属性
- 10.2.3 属性与成员变量(字段)的比较
- 10.2.4 属性的新特性 (>4.6)
- 10.2.5 类属性也是一种类型的场景
- 10.3 类的访问修饰符
- 10.4 方法
- 10.4.1 构造方法
- 10.4.2 类初始化的步骤
- 10.4.3 this关键字在类中的两个作用
- 10.4.4 对象初始化器
- 10.4.5 实例方法
- 10.4.6 静态方法
- 10.4.7 不确定参数
- 10.4.8 垃圾回收机制
- 11 泛型集合
- 11.1 集合类型
- 11.2 字典类型
- CSharp高级进阶
- 1 OOP - 继承 inherit
- 1.1 关键字base this
- 1.2 protected关键字
- 1.3 private 关键字
- 1.4 abstract关键字,抽象方法
- 2 OOP-多态
- 2.1 继承多态
- 3 虚方法
- 4 new关键字
- 5 接口
- 5.1 面向接口编程
- 5.2 创建接口
- 5.3 创建接口实现类
- 5.4 基于接口实现多态
- 6 反射
- 6.1 概念
- 6.2 实现反射
- 7 泛型
- 7.1 List和Dictionary回顾
- 7.2 引入泛型
- 7.3 泛型类、泛型方法、泛型返回值
- 7.4 default关键字
- 7.5 dynamic关键字
- 7.6 泛型约束
- 8 委托
- 8.1 引入委托
- 8.2 使用委托的步骤
- 8.3 委托的基本应用
- 8.4 匿名方法
- 8.5 lambda表达式
- 8.6 自定义泛型委托
C# 基础篇
0 环境部署
安装Visual Studio。
下载地址:https://visualstudio.microsoft.com/zh-hans/
可以选择社区版本,是可以免费使用的。
下载之后配置安装。
按照自己的工作需要,勾选相应的组件和安装位置,进行安装即可。
1 Hello world
1.1 两种框架
目前C#开发的两种框架
运行于windows的.Net Framework
可以跨平台的.Net6
1.2 创建项目的过程
- 项目名称
- 建议:英文命名(公司名称.项目名称)
- 解决方案名称
- 可以和项目名称一样
- 框架版本
- 建议4.6以上(框架版本和VS版本是对应的)
创建一个新项目
- 打开VS,选择创建新项目
-
选择编程语言
-
选择运行平台
-
选择程序类型
web、服务、控制台、库、桌面用的比较多,初学阶段使用控制台程序。
创建一个新项目
至此一个新项目创建完成
编写hello world
// 系统默认引用的命名空间
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 定义命名空间
namespace ZJF.SayHi
{
// 定义类
internal class Program
{
// Main方法是程序的唯一入口
static void Main(string[] args)
{
// 输出Hello world
Console.WriteLine("Hello C# world! ");
// 保持输入
Console.ReadLine();
}
}
}
1.3 项目组成结构
1.3.1 解决方案和项目
解决方案和项目是一对多的关系,一个解决方案下面可以有至少一个或者多个项目,我的理解可能是比较大的系统,分模块开发的过程
1.3.2 程序集信息
程序集信息描述了项目的元信息。比如版权信息,版本信息,程序集名称等。
程序集信息可以从两个位置看到
1 项目文件夹下面的Properties
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("ZJF.SayHi")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ZJF.SayHi")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("fa954032-1d23-4d86-adc3-64505c285241")]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
2 鼠标右键项目名选择属性,也可以通过界面修改程序集信息
1.3.3 引用
引用自己封装的模块或者是第三方的模块,也叫程序集
1.3.4 配置文件
每个项目有且只能有一个,而且不能改名字!
1.3.5 程序入口类
每个项目必须有且只能有一个程序入口,程序入口类必须有一个Main的静态方法。
1.4 解决方案
解决方案就是一系列程序模块的组合,解决方案可以统一编译,同一更新。
生成解决方案:生成exe可执行文件
重新生成解决方案:删除已经生成的可执行文件,重新生成
清除解决方案:清理掉解决方案
1.5 Debug 和 Release
Debug
调试模式,可以打断点,观察程序的执行过程
Release
把已经调试好的程序发布,Release后的程序更加轻量级,编译器内部做了优化
1.6 CSharp虚拟机初始
1.7 .Net Framework混合语言开发
1.8 托管代码和非托管代码
托管代码
在.Net 平台下面写的程序, C# , java
非托管代码
编译之后,直接运行在OS中, C
1.9 公共语言运行时CLR
CLS : 公共语言规范 ,解决不同语言之间的语法问题
CTS: 通用类型系统,解决不同开发语言之间的数据类型差异
2 编程要素
2.1 项目组成
2.2 命名空间和类
在面向对象编程语言中,类是程序的基本单元,用类可以封装要处理的数据(字段、属性)、和具体的处理过程(方法)
一个C#程序的抽象
引入命名空间
namespace 命名空间名称
{
class 类名称
{
字段 属性
构造方法
方法
public 返回值 方法名称(参数)
{
// 方法具体代码
}
}
}
// 引入的命名空间
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 当前类所在的命名空间
namespace ZJF.SayHi
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello C# world! ");
Console.ReadLine();
}
}
}
命名空间原则
- **必须要有:**任何一个类都要属于一个特定的命名空间
- **用于管理:**通过命名空间更好的对“类”进行分类管理
- **自己规划:**一个项目可以根据实际需要,开发者自己划分若干名字空间
- **默认名称:**默认命名空间和项目名称相同
- **使用方法:**用”.“分割,但是不表示包含关系,是层次关系
- **特别注意:**不能以数字、特殊符号开头,不建议用中文
命名空间引入时间
- 两个类所在的命名空间不同
- 其中一个类使用另一个类,则要引入命名空间或者全限定名
- 两个类所在的类库不同
- 先引入类库,在引入命名空间
示例代码一:引用不同命名空间中的类的使用场景
-
关于默认命名空间
默认的命名空间是项目名称,可以通过鼠标右键,选择属性,修改默认的命名空间,要根据实际的项目情况,规划命名空间,命名空间可以让程序更有层次感,结构更清晰。
当然,我们也可以在程序源代码直接手动修改命名空间。
-
项目名/program.cs 这是入口文件
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.SayHi { internal class Program { static void Main(string[] args) { TestClass testClass = new TestClass(); testClass.TestID = 1; testClass.TestName = "test"; TestClass2 testClass2 = new TestClass2(); testClass2.TestID = 2; testClass2.TestName = "test"; } } }
-
项目名/TestClass1.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.SayHi { class TestClass { public string TestName { get; set; } public int TestID { get; set; } } }
-
项目名/TestClass2.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.SayHi.Test2 { internal class TestClass2 { public string TestName { get; set; } public int TestID { get; set; } } }
在这个示例中,TestClass1与Program具有相同的命名空间,TestClass2具有不同的命名空间,如果直接使用TestClass2会直接编译错误:
未找到类型或者命名空间,提醒使用using引入
如果需要使用TestClass2,那么需要在Program中添加命名空间的引用
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
#region 处理命名空间引入错误
using ZJF.SayHi.Test2;
#endregion
namespace ZJF.SayHi
{
internal class Program
{
static void Main(string[] args)
{
TestClass testClass = new TestClass();
testClass.TestID = 1;
testClass.TestName = "test";
TestClass2 testClass2 = new TestClass2();
testClass2.TestID = 2;
testClass2.TestName = "test";
}
}
}
示例代码2:不同的命名空间中有相同名称的类,使用完全限定名
不同的命名空间中,允许有相同名称的类型,但是在其他的命名空间中引用的时候,只能使用全限定名。
全限定名=命名空间.类名
-
项目名/TestClass2.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.SayHi.Test2 { internal class TestClass2 { public string TestName { get; set; } public int TestID { get; set; } } }
-
项目名/TestClass3.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.SayHi.Test3 { internal class TestClass2 { public string TestName { get; set; } public int TestID { get; set; } public void SayHello() { Console.WriteLine($"{this.TestName} say hello"); } } }
-
项目名/Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ZJF.SayHi.Test2; using ZJF.SayHi.Test3; namespace ZJF.SayHi { internal class Program { static void Main(string[] args) { TestClass2 testClass2 = new TestClass2(); testClass2.TestID = 2; testClass2.TestName = "test"; TestClass2 testClass3 = new TestClass2(); testClass3.TestID = 3; testClass3.TestName = "test"; } } }
同时引用两个不同的命名空间,分别是ZJF.SayHi.Test2和ZJF.SayHi.Test3,这两个命名空间中具有相同的类TestClass2。
在Program中引用之后,会出现报错:TestClass2不存在明确的引用。
-
解决方案(使用全限定名)
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.SayHi { internal class Program { static void Main(string[] args) { // 修改成全限定名 ZJF.SayHi.Test2.TestClass2 testClass2 = new ZJF.SayHi.Test2.TestClass2(); testClass2.TestID = 2; testClass2.TestName = "test"; ZJF.SayHi.Test3.TestClass2 testClass3 = new ZJF.SayHi.Test3.TestClass2(); testClass3.TestID = 3; testClass3.TestName = "test"; } } }
全限定名,除能够处理这种不同命名空间相同类名情况,后面还会用于反射中!
2.3 数据类型
-
数值型
- 整数
- byte 无符号 1个字节
- short 有符号 2个字节
- int 有符号 4个字节
- long 有符号 8个字节
- sbyte 有符号1个字节
- ushort 无符号2字节
- uint 无符号4字节
- ulong 无符号8字节
- 浮点数
- float 4字节 7位小数 默认是double 如果定义float需要在后面加上一个f,大写的F和小写的f都可以
- double 8字节 16位小数
- 整数
-
非数值型
-
字符
- 单个字符用单引号表示
-
字符串
- string
-
布尔
- bool
-
日期
-
DateTime
DateTime dt = Convert.ToDateTime("2022-1-1")
-
-
数组
-
集合
-
对象
-
2.4 变量
表示一个数据的存储空间,地址的别名,其实是C语言中的指针,存储变量地址的变量,只是C#弱化了指针的概念。
变量三要素
- 数据类型
- 变量名称
- 变量的值
变量使用方法
- 定义(声明) :规定变量的类型和名称
- 赋值 通过赋值运算符给已声明的变量赋值
- 读取 获取变量的值
变量命名规范
- 组成:英文字母数字下划线
- 开头:只能以字母或者下划线开头
- 禁用:关键字和$
- 名称:见明知意
- 区分:严格区分大小写
- 原则:使用Camel命名法,首字母小写,其他单词首字母大写
类、属性、方法命名
- 原则:Pascal命名法,首字母大写,其他单词字母也大写
- 举例:PersonInfo(类名) PersonID(属性) ShowInfo(方法)
2.5 注释
-
当行注释
// 注释
-
文档注释
namespace ZJF.DataTypeAndVar { internal class Program { /// <summary> /// 入口静态方法 /// </summary> /// <param name="args"></param> static void Main(string[] args) { } } }
类名和方法名和程序上面一定要加一些注释,方便自己理解或者团队开发者理解。
2.6 当前阶段常见错误
2.6.1 命名空间和类名错误
- 命名空间或者类名写错,没有明确区分大小写
- 类中嵌套类
- 没有引入类库
示例代码,在同一个解决方案中创建一个类库,并引用该类库
右键解决方案,添加新项目。创建一个新的类库。
类库的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//类库中Class1类所在的命名空间是TestDLL
namespace TestDLL
{
public class Class1
{
}
}
创建成功之后在解决方案下面有两个项目,分别是类库和一个控制台项目。
在控制台项目中使用类库中的类,需要两个步骤:
1 引用类库
2 引入命名空间
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 使用类库的命名空间
using TestDLL;
namespace ZJF.DataTypeAndVar
{
internal class Program
{
/// <summary>
/// 入口静态方法
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
// 实例化类库中的类
Class1 class1 = new Class1();
}
}
}
2.6.2 变量错误
- 变量位声明,先使用
- 变量未赋值,先使用
- 变量名非法,关键字、大小写、特殊字符
3 输入输出
3.1 Write WriteLine
static void Test1()
{
Console.WriteLine("今天天气怎么样?");
Console.Write("天气很好呀");
Console.Write("有什么计划吗?\n");
Console.WriteLine("去郊游那");
}
输出:
今天天气怎么样?
天气很好呀有什么计划吗?
去郊游那
Console.WriteLine 等价于 Console.Write + “\n”
3.2 Read ReadLine
从控制台输入的字符默认是字符串类型,可以通过字符串接收信息。
static void Test2() {
Console.WriteLine("欢迎使用信息录入系统");
Console.WriteLine("请输入姓名:");
string name = Console.ReadLine();
Console.WriteLine("请输入电话:");
string phone = Console.ReadLine();
Console.WriteLine("姓名:"+name+"电话: "+phone);
}
输出:
欢迎使用信息录入系统
请输入姓名:
zhansan
请输入电话:
1231
姓名:zhansan电话: 1231
Console.Read 和 Console.ReadLine于Write和Writeline相同
3.3 字符串格式化方法
static void Test3()
{
string name = "kobe";
int age = 41;
string hobby = "打篮球";
// 直接拼接
string result1 = "姓名:" + name + "年龄:" + age + "爱好:" + hobby;
// 格式化方式
// 使用$符号格式化
string result3 = $"姓名:{name},年龄:{age},爱好:{hobby}";
Console.WriteLine(result1);
Console.WriteLine("姓名:{0},年龄:{1},爱好:{2}", name, age, hobby);
Console.WriteLine(result3);
}
最常用的是使用$符号。
4 运算符
4.1 赋值运算符
int a = 1; 这是一个赋值运算符操作
4.2 算数运算符
static void Test4() {
// 赋值运算符
int a = 10;
int b = 20;
// +
Console.WriteLine($"a+b={a+b}");
// -
Console.WriteLine($"a-b={a - b}");
// *
Console.WriteLine($"a*b={a * b}");
// /
Console.WriteLine($"a/b={a/b}");
// % 求模
Console.WriteLine($"a%b={a%b}");
// += -= *= /= %=
Console.WriteLine($"a+=40={a+=40}");
// ++ --
Console.WriteLine($"a++={a++}");
Console.WriteLine($"a--={a--}");
Console.WriteLine($"--a={--a}");
Console.WriteLine($"++a={++a}");
}
输出结果:
a+b=30
a-b=-10
a*b=200
a/b=0
a%b=10
a+=40=50
a++=50
a–=51
–a=49
++a=50
其他的算术运算符都比较好理解
主要理解一下++ –
++ – 在变量的后面是先输出在进行加减1操作
相反,如果在变量的前面是先加减1操作,然后再进行输出
4.3 比较运算符
大于、小于、等于、大于等于、小于等于
比较运算符一般用于值类型,string被当作值类型使用。
5 类型转换
5.1 值类型之间的转换
自动类型转换
目标类型 > 使用的数据类型
从int转到double,自动类型转换,小范围的类型可以安全的放在大范围的类型。
举个例子:
本来一个装大象的冰箱,现在用这个冰箱装猴子,肯定是可以装下的。
强制类型转换
目标类型 < 使用的数据类型
大范围的数据类型转换成小范围的数据类型。
显示类型转换 int result =(int)a + b;
引用类型需要先转换成浮点类型,然后在转换成整数类型。
代码
-
自动类型转换
static void Test1() { int a = 100; double b = a; Console.WriteLine($"a={a},b={b}"); }
输出:a=100,b=100
小类型的int转换成大类型的double,是自动转换过来的。
-
强制类型转换
static void Test2() { double a = 100; int b = a; Console.WriteLine($"a={a},b={b}"); }
直接报错:无法将类型“double”隐式转换为“int”。存在一个显式转换
static void Test2() { double a = 100; int b = (int)a; // 将double类型强制转换成int类型,丢失部分精度 Console.WriteLine($"a={a},b={b}"); }
输出:a=100,b=100
5.2 值类型与字符串类型之间的转换
理解类型的有效表示形式
如果用一个字符串表示整数类型的值:那么“100”就是整数类型的有效表示形式,“100.5”就不是整数类型的有效表示形式,而是浮点数类型的有效表示形式。
代码
-
字符串转换成值类型
static void Test3() { string a = "100"; string b = "100.5"; int c = int.Parse(a); double d = double.Parse(b); Console.WriteLine($"c={c},d={d}"); }
输出:c=100,d=100.5
-
如果将整数表达形式的字符串转换成浮点数表达形式的字符串,会直接报错
static void Test4() { string a = "100.5"; int b= int.Parse(a); // 输入的字符串格式不正确 Console.WriteLine(b); }
使用两次转换解决这个问题
static void Test5() { string a = "100.6"; int b = (int)double.Parse(a); Console.WriteLine(b); }
输出100
-
值类型转换成字符串类型 ToString
static void Test6() { double a = 100.8; Console.WriteLine(a.ToString()); //100.8 }
5.3 万能转换器Convert
万能转换器同样要求转换的内容是目标类型的有效表示形式
代码
static void Test7()
{
int a = Convert.ToInt32("1");
double b = Convert.ToDouble("100.89");
string c = Convert.ToString(1111);
Console.WriteLine($"a={a},b={b},c={c}");
}
6 程序控制语句
6.1 条件选择 if
使用条件选择一定要知道,逻辑运算符:
- && 逻辑与
- || 逻辑或
- ! 逻辑非
6.1.1 if 语句
语法
if(condition){
// 条件体
}
实际应用
假如有一个网站,需要满足18岁之后才能够观看,可以使用if语句进行测试
static void Test1() {
Console.WriteLine("请输入年龄:");
int age = int.Parse(Console.ReadLine());
if (age >= 18) {
Console.WriteLine("欢迎观看");
return;
}
Console.WriteLine("您无法观看此网页");
return;
}
6.1.2 if-else语句
语法
if(condition){
// 条件体
}else{
// 条件体
}
实际应用
假如考试成绩超过80分,就合格,发奖状,否则不合格,叫家长。
static void Test2()
{
Console.WriteLine("请输入分数:");
int score = int.Parse(Console.ReadLine());
if (score >= 80)
{
Console.WriteLine("考试合格,发奖状!");
}
else {
Console.WriteLine("考试不合格,叫家长!");
}
}
6.1.3 if-else if -else 语句
对考试成绩进行等级划分,分为A,B,C,D四个等级。
static void Test3()
{
Console.WriteLine("请输入分数:");
int score = int.Parse(Console.ReadLine());
char grade;
if (score < 60)
{
grade = 'D';
}
else if (score < 70)
{
grade = 'C';
}
else if (score < 80)
{
grade = 'B';
}
else if (score < 100)
{
grade = 'A';
}
else
{
grade = ' ';
Console.WriteLine("输入不合法!");
}
Console.WriteLine($"本次考试的成绩等级为:{grade}");
}
6.2 混插-关于优先级
对于运算符比较多的时候,用小括号控制优先级
优先级排队
- 最高优先级:小括号()
- 最低优先级:赋值 =
- 优先级排序:!> 算术运算符 > 比较运算符 > && > ||
6.3 三元 运算符
对if…else…结构的简单写法
语法
condition ? 为真的表达式 : 为假的表达式
代码测试
static void Test5() {
int a = 1;
int b = 2;
int max = a > b ? a : b;
Console.WriteLine($"较大值:{max}");
}
6.4 switch
switch与多重if的比较
-
相同点
都是用来处理多分支条件的结构
-
不同点
- 多重if没有switch选择结构的限制,特别适用于某个变量处于某个连续区间的情况
- switch只能处理等值条件判断的情况,而且条件必须是值类型或者字符串类型,字符串类型被当作值类型使用
- switch做等值条件判断,更加高效。
switch 语法
switch(type){
case value:{
// 语句
// break;
}
case value:{
// 语句
// break;
}
default:{
// 语句
// break;
}
}
代码
static void Test6()
{
string choice = Console.ReadLine();
switch (choice)
{
case "A":
{
Console.WriteLine("选择A");
break;
}
case "B":
{
Console.WriteLine("选择B");
break;
}
case "C":
{
Console.WriteLine("选择B");
break;
}
case "D":
{
Console.WriteLine("选择B");
break;
}
default: {
Console.WriteLine("不合法");
break;
}
}
6.5 for 循环
固定循环次数使用for
代码
-
使用for循环计算1-100的累加和
static void Test2() { int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } Console.WriteLine(sum); }
输出为:5050
-
双层循环
假设有一个场景, 我们给三个人测量身高,为保证测量的准确性,我们每个人测试三次,求平均值,最终输出身高最高那个人的身高值。
static void Test3() { double[] avgArr = new double[3]; for (int i = 0; i < 3; i++) { double avg, total = 0; for (int j = 0; j < 3; j++) { Console.WriteLine($"请输入第{j}个人的身高:"); double h = double.Parse(Console.ReadLine()); total += h; } avg = total / 3; Console.WriteLine($"第{i}个人的平均身高:{avg}"); avgArr[i] = avg; } double max = 0; foreach (var item in avgArr) { if (item > max) { max = item; } } Console.WriteLine($"身高最大值:{max}"); }
6.6 while 循环
while 循环适用于不固定循环次数的场景。
代码
-
通过flag确定循环的启动和停止
while (flag) { // 循环体 }
6.7 do…while 循环
do…while 无论怎样至少执行一次循环,while循环条件判断失败一次都不会执行。
7 字符串
7.1 字符串的常用方法
IndexOf,返回字符串中一个字符的索引位置,找不到返回-1
注意: 索引的初始位置是0.
- 使用场景:截取字符串的某个部分,配合Substring方法
static void Test4() {
string email = "1032141242135@qq.com";
int a = email.IndexOf('@');
int b = email.IndexOf("qq.com");
Console.WriteLine($"@ index is {a}; qq.com index is {b}"); // @ index is 13; qq.com index is 14
}
Length,返回字符串的长度
注意:空格也是一个字符
static void Test4() {
string email = "1032141242135@qq.com";
int a = email.IndexOf('@');
int b = email.IndexOf("qq.com");
Console.WriteLine($"@ index is {a}; qq.com index is {b}"); // @ index is 13; qq.com index is 14
Console.WriteLine("+++++++++++++++++++++++++++++++++++++++");
int c = email.Length;
Console.WriteLine($"email len = {c}"); // email len = 20
}
Substring 字符串截取,从一个长字符串截取一部分字符串
-
语法
string result = xxx.SubString(startIndex, endIndex)
-
代码
static void Test5() { string email = "12421341@163.com"; string number = email.Substring(0, 8); Console.WriteLine(number); // 12421341 }
static void Test5() { string email = "12421341@163.com"; string number = email.Substring(0, 8); Console.WriteLine(number); Console.WriteLine("+++++++++++++++++"); string number1 = email.Substring(0, email.IndexOf('@')); // 12421341 Console.WriteLine(number1); string type = email.Substring(email.IndexOf('@') + 1); // 163.com Console.WriteLine(type); }
使用IndexOf配合Substring可以很好的解决获取字符串。
字符串比较
注意:区分大小写
-
常用比较方法 ==
static void Test6() { string name1 = "kobe"; string name2 = "Kobe"; string name3 = "kobe"; Console.WriteLine("name1==name2 "+ (name1==name2)); // 注意这里必须加括号,因为优先级问题 Console.WriteLine("name1==name3 " + (name1 == name3)); }
输出结果:
name1name2 False
name1name3 True -
使用方法比较 xxx.Equals
static void Test6() { string name1 = "kobe"; string name2 = "Kobe"; string name3 = "kobe"; Console.WriteLine("name1==name2 " + (name1 == name2)); Console.WriteLine("name1==name3 " + (name1 == name3)); Console.WriteLine("name1==name2 " + name1.Equals(name2)); // 这里不用加括号,因为对象.方法的优先级高 Console.WriteLine("name1==name3 " + name1.Equals(name3)); }
这里的输出结果和刚刚的输出结果一样的。
ToLower 转换成小写
对于有些忽略大小写的场景比较,可以把两个比较的字符串统一变成大写,或者统一变成小写。
static void Test7() {
string s1 = "fsdaFFkkfdsagvgKK";
string s1ToLower = s1.ToLower();
string s1ToUpper = s1.ToUpper();
Console.WriteLine(s1ToUpper);
Console.WriteLine(s1ToLower);
}
ToUpper转换成大写
同上
Equals 比较方法
注意:== 或者 Equals方法默认只能比较”值类型“和”字符串类型“,对象类型是不能进行比较的
Trim 前后去空格
TrimStart 获取字符串前面的空格
TrimEnd 获取字符串后面的空格
LastIndexOf 匹配最后一个字符的位置
static void Test8()
{
string email = "123@qq.com@163.com";
int index = email.LastIndexOf('@');
Console.WriteLine(index); // 10
}
7.2 字符串格式化
7.3 空值&空对象
7.3.1 空值
定义空值的两种方式:
-
方式1
string empty = ""
-
方式2
string empty = string.Empty
-
代码
static void Test9() { string s1 = ""; string s2 = string.Empty; Console.WriteLine("s1==s2" + (s1 == s2)); // True }
7.3.2 空对象
空对象被赋值为null
null 关键字是一个空值
null与”“是两回事、
不能使用null调用方法
未将对象引用到对象的示例
7.4 高效处理字符串
StringBuilder类
7.5 转义字符
反斜杠 \ “.\\Envs\\Python”
使用但反斜杠的场景都需要转义。
@ @“.\Envs\Python”
注意:写到配置文件中不需要转义
如何使用配置文件中的配置内容
- 添加引用 System.Configurations
- using 命名空间
- ConfiguationManager.ConnectionStrings[“connString”].ToString()
7.6 字符串分割与连接
分割 Split
组合 Join
static void Test10() {
string s1 = "AA BB CC DD";
string[] arr = s1.Split(); // 默认使用空格分割
foreach (var item in arr)
{
Console.WriteLine(item);
}
string s2 = string.Join("-", arr);
Console.WriteLine(s2);
}
结果:
AA
BB
CC
DD
AA-BB-CC-DD
8 数组
8.1 基本概念
定义
数组也是一个变量,是一个具有连续空间,并且能够存储相同类型数据的空间
基本要素
- 标识符: 数组名
- 元素:数组内的元素名
- 类型:数组内元素的类型,必须是相同类型
- 下标:数组中元素的索引
操作数据的步骤
-
声明数组
// 语法 类型[] 数组名 int[] scores // 声明一个数组,注意,只是声明。
-
数组实例化
数组需要实例化,数组也是一个类型,所以需要分配空间。必须写清楚数组长度,否则不知道分配多大的空间
// 语法 // 已声明 scores = new int[5]; // 未声明 int[] scores = new int[5];
-
数组赋值
// 通过下标赋值: int[0] = 10 int[1] = 11 int[2] = 12 int[3] = 14 int[4] = 16 // 声明+实例化+元素赋值放在一行中,适用于数组元素已经确定的场景 方式1: int[] scores = new int[3]{1,2,3} 方式2: int[] scores = new int[]{1,2,3,4,5} 方式3: int[] scores = {1,2,3,4,5}
8.2 数组遍历
for 循环
-
语法
for (int i =0; i < arr.Length; i++){ arr[i] // 对元素操作 }
-
实际代码,对数组中的每个元素加1,随后在输出
foreach循环
不关注索引,使用foreach
-
语法
foreach(int item in arr){ item // 就是每一个元素类型 }
-
实际代码
8.3 值类型与引用类型
值类型
基本数据类型再传递变量值时,传递的是变量的副本,开辟新的空间。
引用类型
引用类型变量传递自己,也就是变量本身,没有开辟新空间
常用的引用数据类型:
- string字符串(.Net做了特殊包装,属于引用类型,但是需要当值类型来使用)
- 数组
- 系统库中的各种类
- 自定义的类
8.4 底层类和关键字
String是.Net Framework的类
string是C#的关键字,也是String类的别名,表示一种数据类型,映射为.Net中的String
使用string时编译器会把它编译成String
string关键字,不能作为类、结构、枚举、字段、变量、方法、属性的名字,而String不是关键字,可以使用
9 常量枚举
9.1 常量
一个值不希望被别人修改的时候,可以使用常量
常量名建议全部大写
常量必须在定义的同时初始化,不能先定义后初始化
-
语法
const double PI = 3.14
-
实际测试:
static void Test11() { const double PI = 3.14; PI = 3.141; // 报错 Console.WriteLine(PI); }
9.2 枚举
基本概念
作用:可以用来定义”有限个变量值“,方便选择使用,避免出错
特点:可以变送一组描述性的名称,还可以有对应的整数值
语法
// 定义
public enum Gender{
Mela = 1, //整数值可以有也可以没有。
Female = 0
}
// 使用
Gender gender = Gender.Male
gender = "m" // 非法赋值,必须是枚举中的一个元素。
实际应用
internal class Program
{
static void Main(string[] args)
{
Test1();
Console.Read();
}
static void Test1() {
Gender gender = Gender.Femela;
Console.WriteLine(gender);
Console.WriteLine((int)gender);
gender = Gender.Mela;
Console.WriteLine(gender);
}
}
public enum Gender{
Mela = 1,
Femela = 0
}
public enum Week {
Sun,
Sat,
Fri,
Sur,
Wed,
Thu,
Mon
}
}
输出结果:
Femela
0
Mela
面向对象Basic
10 类与对象
10.1 概念
编程大概有两种方式,分别是面向过程和面向对象。
面向过程可以实现所有的需求,但是面向过程的程序缺少模块化。
面向对象有两个比较重要的概念,分别是类和对象,类也是一种数据类型,描述了一类对象中的所有属性和方法。类主要是抽象能力。
-
对象:
- 静态属性:描述对象的基本特征信息
- 动态行为:描述这个对象能做什么,如何做
-
类中的内容
- 类中可以有字段、属性、方法
- 不建议类中只有字段
- 类中可以只有属性、也可以只有方法
定义类的语法
-
定义类 类名使用Pascal命名法:首字母必须大写.属性和方法一般首字母大写,字段首字母小写
访问修饰符 关键字 类名 public class Person { // 字段 // 属性 // 方法 }
-
创建对象
// 类型 变量名 = 关键字 类名() 构造函数 Person person = new Person();
-
获取属性
person.Attr
-
设置属性
person.Attr = "xxxx"
-
调用方法
person.Method()
类的访问修饰符
-
程序集的概念
一个类库,或者一个exe可执行文件,叫做一个程序集
-
public
public允许类被程序集外部访问
-
internal
internal不允许被程序集外部访问
10.2 属性Property
小tips
- 在VS中,prop 连续两个tab键,会生成属性
10.2.1 创建一个类,并实例化
成员变量(字段)和局部变量
-
成员变量(字段)
成员变量设置在类内方法外,成员变量一般用于类内部使用,一般设置访问优先级为private,不建议设置public的成员变量
-
局部变量
局部变量在类中的方法内
-
代码
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { internal class Person { private string _desc = string.Empty; // 成员变量(字段) public int PId { get; set; } public string Job { get; set; } public string Name { get; set; } public void ShowInfo() { int name = "Any"; // 局部变量 Console.WriteLine($"PId={PId},Job={Job},Name={name}"); } } }
创建类,并实例化
-
创建类
在项目中创建一个People类。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { internal class People { public int PId { get; set; } public string Name { get; set; } public string Description { get; set; } public void ShowInfo() { Console.WriteLine($"PId={PId},Name={Name},Desc={Description}"); } } }
-
实例化类
在Program类中访问Person类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { public class Program { static void Main(string[] args) { Test1(); Console.Read(); } static void Test1() { People people= new People(); people.PId = 1; people.Name = "Test"; people.Description = "Description"; people.ShowInfo(); } } }
输出为:PId=1,Name=Test,Desc=Description
使用ildasm 查看C#的字节码
ildasm是一个反编译工具,通过这个工具可以把C#的源代码反编译成C#的中间语言(其实就是C#)的字节码。使用C#虚拟机运行的字节码。
所在位置:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools
ildasm软件的空白图
查看源代码的文件夹:
将debug文件夹中的可执行文件exe拖入到IL DASM中:
点击任何一个组件可以看到里面的字节码:
自动为每个属性定义了一个成员变量,也可以手动设置成员变量用于保存属性的值
属性只是一个接口,真正保存数据的是字段
-
手动定义字段来保存属性的值
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { internal class People { private int _pid = 0; // 定义成员变量并赋初始值为0 public int PId { get { return _pid; // 属性的get方法返回成员变量的值 } set { _pid= value; // 属性的set方法设置成员变量的值 } } public string Name { get; set; } public string Description { get; set; } public void ShowInfo() { Console.WriteLine($"PId={PId},Name={Name},Desc={Description}"); } } }
-
查看手动设置成员变量反编译的结果
注意:编译器没有为PId属性创建新的k__BackingField字段,而是使用了手动的 _pid成员变量。
-
自动属性与手动设置成员变量与属性关联具有相同的效果
public int PId {get; set;} //等价于 private int _pid = 0; public int PId { get { return _pid; } set { _pid = value // 这里的value是编译器确定的 } } // 当调用obj.PId 时 返回 _pid 成员变量的值 // 当调用obj.PId = xxx 给属性赋值时,将xxx赋值给_pid
10.2.2 自动属性
在C# 3.0版本开始使用自动属性,在之前的版本都是通过手动与成员变量做关联。
在使用断点调试时,当给属性赋值时,调用了set方法,给对应的成员变量赋值,当读取属性值时,调用了get方法,从对应的成员变量获取值。
属性本身并没有保存任何数据,数据是保存到属性对应的私有成员变量中。
属性的作用是访问对象数据的入口。
10.2.3 属性与成员变量(字段)的比较
成员变量(字段)
-
字段是为类的内部方法、或者方法之间传递数据使用,强调对内使用
-
访问修饰符:一般成员变量都是用private修饰,基本不用public
-
读写:字段读写均可,也可以用readonly限制为只读,但是不能添加业务逻辑,比如一个人的身高,用户输入超过3米,显然是不合理的,这种使用字段就很难限制。
-
代码
可以直接获取成员变量和设置成员变量的值
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { internal class People { //public readonly int _age; public int _age; } }
static void Test2() { People people = new People(); people._age = 11; // 直接设置成员变量的值 Console.WriteLine(people._age); // 获取成员变量的值 }
可以或者只读属性的值,但是不能设置只读属性的值
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { internal class People { public readonly int _age; //public int _age; } }
static void Test2() { People people = new People(); people._age = 11; // 无法分配到只读字段 直接编译无法通过 Console.WriteLine(people._age); }
属性
-
属性是对外提供数据访问、本身不保存数据,强调对外使用,对外部的接口
-
修饰符:属性都是用public修饰、从来不用private
-
读写:属性可以轻松的实现单独读写控制,并且可以添加任意需要的逻辑
-
对于一些场景,必须使用属性,比如dgv列表必须强制使用属性
-
代码
为属性添加业务逻辑,比如人类,年龄这个属性,不允许超过100岁,超过100直接设置成100
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml.Schema; namespace OopProperty { internal class People { private int _age; public int Age { get { return _age; } set { if (value > 100) { _age = 100; } else { _age = value; } } } } }
static void Test3() { People people = new People(); people.Age = 101; Console.WriteLine(people.Age); People people1 = new People(); people1.Age = 11; Console.WriteLine(people1.Age); }
同样可以设置只读属性,把set删除即可
10.2.4 属性的新特性 (>4.6)
属性直接初始化
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Schema;
namespace OopProperty
{
internal class People
{
public string Name { get; set; } = "张三";
}
}
static void Test4() {
People people = new People();
Console.WriteLine(people.Name); // 张三
people.Name = "里斯";
Console.WriteLine(people.Name); // 里斯
}
属性表达式,设置只读属性的三种不同方式
-
直接使用箭头函数
public class People{ public int Age {get=>100;} }
-
变种方式2
public class People{ public int Age =>100;} }
-
变种方式3
public class People{ public int Age {get;} = "111" }
10.2.5 类属性也是一种类型的场景
一个类的属性可以是另一个类
比如一个汽车类,有一台发动机,发动机也是一种类。
-
定义发动机类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { internal class Turbo { public float Volumn { get; set; } public string Type { get; set; } } }
-
定义汽车类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopProperty { internal class Car { public string Brand { get; set; } public Turbo CarTurbo { get; set; } public void ShowInfo() { Console.WriteLine($"汽车品牌:{Brand},发动机型号:{CarTurbo.Type},发动机排量:{CarTurbo.Volumn}L" ); } } }
-
调用方代码
static void Test5() { Turbo ea888 = new Turbo(); ea888.Type = "EA888"; ea888.Volumn = 2.0f; Car audiA6 = new Car(); audiA6.Brand = "audi"; audiA6.CarTurbo = ea888; audiA6.ShowInfo(); }
输出:汽车品牌:audi,发动机型号:EA888,发动机排量:2L
10.3 类的访问修饰符
测试访问修饰符,类的默认访问修饰符为internal
-
在解决方案中创建一个新的类库
在解决方案中有一个控制台的项目有一个类库的项目,类库的项目和控制台项目分别属于两个不同的程序集
在类库项目中随便创建一个类,定义为internal类型
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ClassLibrary1 { internal class Class1 { public int MyProperty { get; set; } } }
-
在控制台项目中引入类库,引用命名空间,使用类
static void Test6() { Class1 c1 = new Class1(); // Class1不可访问,一位内它具有一定的保护级别 }
-
在控制台项目中使用类库中的public类
namespace ClassLibrary1 { public class Class1 { public int MyProperty { get; set; } } }
修改访问修饰符之后,就不再报错了。
10.4 方法
10.4.1 构造方法
定义
构造方法是在对象创建的时候,调用的方法,通过构造方法对实例进行初始化。
Object obj = new Object()
// 大家都知道,通过new关键字,创建一个类的实例,也叫类的实例化
// new关键字其实就是在堆中开辟了一块内存
// Object()就是调用类的构造方法,在构造方法中,可以对实例进行初始化
类实例化的过程可以分割成两步,第一步是开辟内存,第二步是初始化
一个类中,在我们没有添加任何构造方法的时候,编译器会生成一个无参数的构造方法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OopMethod
{
internal class Person
{
}
}
通过IL DSAM反编译工具,查看反编译后的结果,一个空白类中会默认有一个无参数无返回值的构造方法,当然了,所有的构造方法都不能有返回值。
- 构造方法小结:
- 构造方法必须和类名一致
- 构造方法不能有任何返回值
- 构造方法可以有参数,也可以没有参数
- 构造方法支持重载,重载的条件是参数类型或者参数个数不一样
创建构造方法
-
语法
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopMethod { internal class Person { public Person() { Console.WriteLine("无参数构造方法"); } } }
在另外的一个类中,实例化这个类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopMethod { internal class Program { static void Main(string[] args) { Test1(); Console.Read(); } static void Test1() { Person person = new Person(); } } }
打印:无参数构造方法
这就说明,在实例化的过程中,执行了构造方法。
-
构造方法可以重载
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopMethod { internal class Person { public Person() { Console.WriteLine("无参数构造方法"); } public string Name { get; set; } = string.Empty; public int Age { get; set; } = 0; public Person(string name) { Console.WriteLine("有一个name参数的构造方法"); Name = name; } public Person(string name,int age) : this(name) { Console.WriteLine("有两个参数的构造方法"); Age = age; } } }
-
构造方法可以进行嵌套
public Person(string name,int age) : this(name) { Console.WriteLine("有两个参数的构造方法"); Age = age; } // 构造方法:this() 会根据this中参数的不同,调用其他的不同的构造方法
10.4.2 类初始化的步骤
-
赋值的成员变量
-
显示声明的属性
-
构造方法
-
代码:
注意:下面代码注释中标注的数字代码初始化的顺序
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopMethod { internal class Person { private int _id = 1000; // #1 public Person() { Console.WriteLine("无参数构造方法"); } public string Name { get; set; } = string.Empty; // #2 public int Age { get; set; } = 0; // #3 public Person(string name) { Console.WriteLine("有一个name参数的构造方法"); Name = name; } public Person(string name, int age) : this(name) // #4 { Console.WriteLine("有两个参数的构造方法"); Age = age; } } }
成员变量必须先初始化,否则属性就无法初始化,因为属性的数据存储在成员变量中,不管是自动属性还是手动设置的属性。
10.4.3 this关键字在类中的两个作用
一个构造方法调用其他的构造方法
如果一个类中,有三个构造方法:
分别是:
无参构造方法
有一个参数的构造方法,对应Name属性
有两个参数的构造方法,对应Name属性和Age属性
public class Person{
public string Name{get;set;}
public int Age {get; set;}
public Person(){
}
public Person(string name){
Name = name;
}
public Person(string name, int age){
Name = name; // 这行其实就是一个参数的构造函数
Age = age;
}
// 这种情况下会出现很多重复的代码
// 对于这种情况可以使用以下情况进行简化:
public Person(string name, int age):this(name){
Age = age;
}
}
在类中通过this访问属性、字段、方法
this 指代的其实就是当前类的一个实例对象,通过this关键字,可以获取成员变量、属性和方法
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace OopMethod
{
internal class Student
{
public int Id { get; set; }
public string Name { get; set; }
private string _hobby;
public string Hobby
{
get
{
return this._hobby;
}
set
{
this._hobby = value;
}
}
public Student() { }
public Student(int id, string name, string hobby) {
this.Id= id; // this.Id 调用的是属性
this.Name = name;
this.Hobby = hobby;
}
public void Method1()
{
this.Method2();
}
public void Method2()
{
Console.WriteLine($"{this.Id},{this.Name},{this.Hobby}");
}
}
}
10.4.4 对象初始化器
对象初始化器是显示的给对象进行初始化,对象初始化器与对象内部的构造方法需要配合使用,而不是相互替代的关系。
-
语法
Person person = new Person(){ Id = 1000, // 注意。这里是逗号 Name = "稀罕" }; // 注意,这里的;一定要加
-
实际使用
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopMethod { internal class Car { public Car() { } public Car(string type) { Type = type; } public string Type { get; set; } public string Year { get; set; } public void ShowInfo() { Console.WriteLine($"{this.Type}:{this.Year}"); } } }
调用方代码:
static void Test3() { Car car = new Car("audi") { Type ="bmw", // 对象初始化器覆盖了构造函数初始化的属性值 Year = "2020" }; car.ShowInfo(); // bmw:2020 }
10.4.5 实例方法
语法
方法名使用Pascal命名法,必须首字母大写
访问修饰符 返回值类型 方法名(参数列表){
// 方法体
}
代码
-
无参数,没有返回值
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopMethod { internal class Car { public Car() { } public Car(string type) { Type = type; } public string Type { get; set; } public string Year { get; set; } public void ShowInfo() { Console.WriteLine($"{this.Type}:{this.Year}"); } } }
-
无参数,有返回值
public string OutInfo() { string info = $"{Type}:{Year}"; return info; }
使用调用方进行调用:
static void Test4() { Car car = new Car("audi"); car.Year = "1111"; string info = car.OutInfo(); Console.WriteLine(info); }
-
有参数,没有返回值
public void HasParamNoRet(int start) { start += 100; Console.WriteLine($"{start}"); }
调用
-
有参数,有返回值
public int HasParamHasRet(int start) { return start += 100; }
调用
static void Test5() { Car car = new Car("奔驰"); car.Year = "1141"; car.HasParamNoRet(11); int result = car.HasParamHasRet(11); Console.WriteLine("result="+result); }
-
返回数组
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OopMethod { internal class Tyre { public Tyre() { } public Tyre(int addr) { Addr = addr; } public int Addr { get; set; } } }
public Tyre[] TyreArray() { Tyre t1 = new Tyre(1); Tyre t2 = new Tyre(2); Tyre t3 = new Tyre(3); Tyre t4 = new Tyre(4); Tyre[] Tyres = new Tyre[4]; Tyres[0] = t1; Tyres[1] = t2; Tyres[2] = t3; Tyres[3] = t4; return Tyres; } }
-
返回对象
public Engine GetEngine() { Engine engine= new Engine("ea888"); return engine; }
-
调用方代码
static void Test6() { Car car = new Car("沃尔沃"); Tyre[] results = car.TyreArray(); foreach (var item in results) { Console.WriteLine($"轮胎位置:{item.Addr}"); } Console.WriteLine("____________________________________"); Engine e = car.GetEngine(); Console.WriteLine(e.Name); }
输出:
轮胎位置:1
轮胎位置:2
轮胎位置:3
轮胎位置:4
ea888
10.4.6 静态方法
定义
实例方法是通过对象.实例方法进行调用的
静态方法是通过类名.静态方法进行调用的
静态方法的特点是不需要构建对象就可以直接使用。
静态方法跟随类一起加载,被加载在栈空间,并且不会被垃圾回收,直到程序退出执行。
静态方法中不能访问实例字段、方法、属性。只能访问静态字段、属性。
10.4.7 不确定参数
当一个方法的参数不固定时,用params关键字实现可变参数
语法
修饰符 返回类型 方法名(params 类型[] 变量名){
// 方法体
}
使用不确定参数的注意点:
- 该参数必须放到方法参数最后,并且只能用一次
- 参数数组必须是一维数组
- params不能配合ref和out组合使用
测试代码
public void ShowInfo2(int a, string[] info) {
foreach (var item in info)
{
Console.WriteLine(item);
}
}
调用方:
static void Test7() {
Car car = new Car("夏利");
car.Year = "1990";
car.ShowInfo2(1,new string[] { "11", "12", "13" });
}
使用params关键字:
public void ShowInfo3(int a, params string[] info)
{
foreach (var item in info)
{
Console.WriteLine(item);
}
}
调用方:
static void Test7() {
Car car = new Car("夏利");
car.Year = "1990";
car.ShowInfo3(1,"11", "12", "13" );
}
10.4.8 垃圾回收机制
.Net虚拟机自带垃圾回收机制,不需要程序员操作。
11 泛型集合
泛型(generic)是编程语言的一种风格,要求程序员明确特定的类型在进行初始化工作。
11.1 集合类型
数组和集合类型比较相似,集合比数组更加灵活,可以动态的扩展。
语法
//List<type>
List <string> nameArr = new List<string>(); // 创建一个实例
常用操作
static void Test8()
{
// 实例化一个集合对象
List<string> nameList = new List<string>();
// 增加元素
nameList.Add("张三");
nameList.Add("李四");
// 获取元素的个数
Console.WriteLine("当前List的长度是:" + nameList.Count);
// 集合的初始化器
List<string> player = new List<string>() { "kebi", "james", "curry" };
// 集合转换成数据
string[] res = player.ToArray();
// 将数组放到这个结构中
player.AddRange(new string[] { "haga发生", "fasfa范德萨" });
// 集合添加数组
string[] names = { "柯布", "柯南" };
// 删除其中一个
player.Remove("haga发生");
player.AddRange(names);
// 按照索引删除
player.RemoveAt(1);
// 在指定位置插入元素
player.Insert(1, "张三"); // 在索引1的位置,也就是第二个元素,插入张三
// 判断一个元素是否在这个集合内
bool isExists = player.Contains("张三");
// 清除所有:
player.Clear();
}
集合初始化器
static void Test9() {
List<string> names = new List<string>() {
"张三",
"李四",
"王五",
"马六"
};
foreach (var item in names) {
Console.WriteLine($"{item}");
}
}
输出:
“张三”,
“李四”,
“王五”,
“马六”
集合复制
static void Test9()
{
List<string> names = new List<string>() {
"张三",
"李四",
"王五",
"马六"
};
List<string> newNames = new List<string>(names);
foreach (var item in names)
{
Console.WriteLine($"{item}");
}
foreach (var item in newNames)
{
Console.WriteLine($"new-{item}");
}
}
输出:
张三
李四
王五
马六
new-张三
new-李四
new-王五
new-马六
11.2 字典类型
语法
Dictinary<k,v> infos = new Dictinary<k,v>();
一般k都是字符串类型的
v可以是对象,也可以是别的类型
Dictinary的结构:
k1:v1
k2:v2
k3:v3
字典初始化器
static void Test10() {
Dictionary<string , string> infos = new Dictionary<string, string>() {
["张三"] = "喜欢读书",
["李四"] = "喜欢唱歌"
};
foreach (var item in infos.Keys) {
Console.WriteLine($"keys:{item}");
}
foreach (var item in infos.Values) {
Console.WriteLine($"values:{item}");
}
}
输出:
keys:张三
keys:李四
values:喜欢读书
values:喜欢唱歌
字典的常用方法与集合的常用方法是相同的
CSharp高级进阶
1 OOP - 继承 inherit
概念
类与类之间的继承,主要是继承属性和方法,比如生活中的祖辈与父辈之间的继承。
继承必须具有单根性,一个类只能有一个父类,不能同时继承多个类,也不能循环继承和反向继承
代码测试1
-
Person.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._01_继承 { internal class Person { public Person() { } public string name { get; set; } public int age { get; set; } public string gender { get; set; } public void doEat() { Console.WriteLine("吃饭ing..."); } } }
-
Student.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._01_继承 { internal class Student:Person { public Student() { } public string School { get; set; } public void study() { Console.WriteLine($"{this.name} is study in {this.School}"); } } }
-
Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ZJF.advance._01_继承; namespace ZJF.advance { internal class Program { static void Main(string[] args) { Test1(); Console.ReadLine(); } static void Test1() { Student s1 = new Student(); s1.name = "张三"; s1.age = 1; s1.gender = "男"; s1.School = "希望学校"; s1.study(); } } }
-
这套代码的执行流程如下:
1. 先执行Student的无参构造,然后执行Person的无参构造
测试代码2
有参构造方法:
-
Person.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._01_继承 { internal class Person { public Person() { } public Person(string name) { this.name = name; } public Person(string name, int age):this(name) { this.age = age; } public Person(string name, int age, string gender):this(name,age) { this.gender = gender; } public string name { get; set; } public int age { get; set; } public string gender { get; set; } public void doEat() { Console.WriteLine("吃饭ing..."); } } }
-
Student.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._01_继承 { internal class Student:Person { public Student() { } public Student(string name, int age, string gender, string school) : base(name, age, gender) { this.School = school; } public Student(string school) { this.School = school; } public string School { get; set; } public void study() { Console.WriteLine($"{this.name} is study in {this.School}"); } } }
-
Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ZJF.advance._01_继承; namespace ZJF.advance { internal class Program { static void Main(string[] args) { Test1(); Console.ReadLine(); } static void Test1() { Student s1 = new Student("李四",2,"nv","清华大学"); s1.study(); } } }
-
代码执行顺序:
1. Student s1 = new Student("李四",2,"nv","清华大学"); 2. 3.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AMczgdu1-1684052960420)(D:\markdown\C#系列\CSharp高级进阶.assets\image-20230429210033497.png)]
【1】子类构造方法没明确调角父类的哪个构造方法时,默认调用父类的无参数构造方法。(前提:父类必须要有无参数的构造方法)
【2】父类没有无参数的构造方法,子类构造方法,必须显式的说明调用父类的哪个有参数的构造方法。
【3】注意:调用父类的构造方法,请使用base关键字。
1.1 关键字base this
this关键字
this关键字表示当前类的实例,在构造方法中用于指向当前类的实例
-
用法1:在构造方法中
-
用法2: 在构造方法传递中
-
用法三:在类内部调用内部的属性或者方法
base关键字
base关键字表示调用当前类的父类中的属性或者方法
- 用法1:调用父类中的属性
- 用法2:调用父类中的构造方法
1.2 protected关键字
protected关键字修饰的属性或者方法,只能在父类或者子类类内使用,不能在对象中使用
1.3 private 关键字
private关键字是私有对象,只能在父类中使用,子类不可见
1.4 abstract关键字,抽象方法
抽象方法必须在抽象类中
-
Person.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._03_kw_abstract { // 抽象方法必须放在抽象类中,否则会报错 public abstract class Person { public string name { get; set; } public string gender { get; set; } public Person() { } public Person(string name, string gender) { this.name = name; this.gender = gender; } // 声明抽象方法,抽象方法在父类中不需要写明函数体 // 抽象方法的目的是规范子类的方法名,形参、和返回值,如果不是抽象方法,子类可以重写也也可以不重写 public abstract void Play(); } }
子类实现
-
子类必须重写抽象类的抽象方法
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._03_kw_abstract { public class Student:Person { public override void Play() { Console.WriteLine("学生放学后开始玩耍!"); } } }
-
如果非抽象子类不重写抽象方法会报错:
using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._03_kw_abstract { public class Student:Person { //public override void Play() { // Console.WriteLine("学生放学后开始玩耍!"); //} } } //错误 CS0534 “Student”不实现继承的抽象成员“Person.Play()” ZJF.advance // //D:\code\csharp_home\ZJF.advance\ZJF.advance\03_kw_abstract\Student.cs 9 活动
-
如果子类是抽象类可以不重写抽象方法
namespace ZJF.advance._03_kw_abstract { public abstract class Student:Person { } } // 如果子类也是抽象类,那么可以不实现抽象方法
-
注意:
抽象方法不能直接new
抽象方法必须在抽象类中
抽象类中可以没有抽象方法
2 OOP-多态
同一个抽象方法,不同子类实现,由父类实例调用
2.1 继承多态
引言
多态是面向对象的很重要特性,通过多态可以很好的实现面向对象编程中的开-闭原则。
开:增加类:通过增加不同的功能类实现需求的变化
闭:修改类:尽量不因为需求的变化修改类的内部
-
多态的基本实现原理
// 父类类型对象调用抽象方法,可以访问到不同子类的实现 public abstract class Car{ public abstract void run(); } // 定义两个不同品牌的汽车继承自Car类 public class Audi:Car{ public override void run(){ Console.WriteLine("奥迪因为Quattro跑的很快!"); } } public class BMW:Car{ public override void run(){ Console.WriteLine("宝马的操控性很好!") } } public class Program{ public static void main(){ Car car = new Audi(); car.run(); // 具体执行哪个run是根据等号右边的实例对象决定的 // 输出:奥迪因为Quattro跑的很快 car = new BMW(); car.run(); // 输出:宝马的操控性很好 } }
继承多态的两种实现方式
多态的目的更多的是为了系统的扩展性。
比如一个关系数据库管理系统,可以对接不同的关系数据库,可以这样实现:
-
定义一个数据库抽象父类,并且定义抽象方法,实现两个子类
public abstract class Database{ public abstract void runSQL(); } public class MysqlDB:Database{ public override void runSQL(){ Console.WriteLine("Mysql数据库执行SQL"); } } public class PgsqlDB:Database{ public override void runSQL(){ Console.WriteLine("PgSQL数据库执行SQL") } }
-
使用类中的方法
public class Program{ public static void main(){ } public static Test1(){ Database db = getDB() db.runSQL() } public static Database getDB(){ return MysqlDB() } }
-
如果需要扩展这个类
// 添加一个新的扩展类 public class OracleSQL:Database{ public override void runSQL(){ Console.WriteLine("Oracle数据库执行SQL") } }
-
使用类中的方法
// 不需要修改Test1这个方法 // 只需要修改getDB()方法即可 public static Database getDB(){ return OracleDB() }
-
子类对象做为方法的返回值
上面数据库中的案例所示
-
父类对象做为方法的形式参数
public static Test1(Database db){ db.runSQL() } // 使用Test1 Test1(MysqlDB())
3 虚方法
父类内部由默认实现的方法,对子类的实现没有强制性,子类可以重写也可以不重写,虚方法也可以实现多态
定义
-
语法
public virtual void FuncName(){ // 虚方法默认的方法体 }
虚方法和抽象方法的使用选择
如果父类需要由默认的实现,使用虚方法,如果父类不需要默认实现,使用抽象方法
子类可以实现虚方法,也可以不实现虚方法,使用父类默认的虚方法实现
-
Father.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._05_虚方法 { internal class Father { public virtual void money() { Console.WriteLine("爸爸有钱都是儿子的"); } } }
-
Son.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._05_虚方法 { internal class Son:Father { public override void money() // 可以重写也可以不重写 { base.money(); // 默认调用父类实现的默认方法,也可以加一些自己的实现 Console.WriteLine("儿子自己也赚了一笔钱!"); } } }
4 new关键字
在子类方法中,使用new关键字,可以实现对父类“同名方法”的覆写。
5 接口
5.1 面向接口编程
接口是规范了一系列方法的集合
5.2 创建接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ZJF.advance._07_接口
{
public interface IPerson
{
void Eat();
void Sleep();
string Speek(string info);
}
}
接口的关键是interface,一般接口名称都是以大写字母I开头,
接口类中只能定义方法,不能实现方法,定义方法的格式如下:
返回值类型 方法名(参数类型 参数名)
接口中的方法不能写仿问修饰符
5.3 创建接口实现类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ZJF.advance._07_接口
{
public class PersonImpl : IPerson
{
public string Name { get; set; }
public PersonImpl() { }
public PersonImpl(string name)
{
this.Name = name;
}
public void Eat()
{
Console.WriteLine($"{this.Name} is eating......");
}
public void Sleep()
{
Console.WriteLine($"{this.Name} is Sleeping......");
}
public string Speek(string info)
{
Console.WriteLine($"{this.Name} is Speeking.....{info}.");
return "hhh";
}
}
}
接口的实现类通常命名为XxxxImpl 表示对IXxxx类的实现。
实现类必须实现接口类的所有方法,所以接口类必须遵循最小化原则,一个接口只负责特定的事情。
5.4 基于接口实现多态
前面讲了通过继承抽象类实现多态,也可以通过接口实现多态。
通过继承实现多态有两种方式:
方式1:抽象类对象做为返回值
static void Test1(){
BaseObj baseobj = GetObj();
baseobj.xxx();
}
static void GetObj(){
return SonObj(); // SonObj必须是BaseObj的子类
}
方式2:抽象类对象做为函数参数
static void Test2(BaseObj obj){
obj.xxx();
}
// 调用者代码:
Test2(SunObj())
-
通过接口实现多态
-
IScreen.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._08_接口2 { /// <summary> /// 屏幕接口类 /// </summary> public interface IScreen { /// <summary> /// 打开 /// </summary> void Open(); /// <summary> /// 关闭 /// </summary> void Close(); /// <summary> /// 表演节目 /// </summary> /// <param name="info"></param> void Show(string info); } }
-
TVScreenImpl.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._08_接口2 { public class TVScreenImpl : IScreen { public void Close() { Console.WriteLine("关闭电视屏幕"); ; } public void Open() { Console.WriteLine("打开电视屏幕"); } public void Show(string info) { Console.WriteLine($"电视屏幕播放:{info}"); } } }
-
PadScreenImpl.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.advance._08_接口2 { public class PadScreenImpl : IScreen { public void Close() { Console.WriteLine("Pad关闭屏幕"); } public void Open() { Console.WriteLine("Pad打开屏幕"); } public void Show(string info) { Console.WriteLine($"Pad查看{info}"); } } }
-
使用层:
static void Test6() { IScreen screen = GetScreen(); screen.Open(); screen.Close(); screen.Show("体育频道"); } static IScreen GetScreen() { //return new TVScreenImpl(); // 只需要返回不同的实现类即可 return new PadScreenImpl(); }
-
使用层:接口对象做为方法
static void Test7(IScreen obj) { obj.Open(); obj.Close(); obj.Show("体育频道"); } // 调用方调用Test7 Test7(new TVScreenImpl()); // 传递对象到 Test7(new PadScreenImpl());
6 反射
6.1 概念
通过使用一个“程序集”把我们要使用得类的“命名空间、类名”通过“字符串”方式,使用反射技术,从而得到这个类的对象。
创建类的对象,有两种方式:
-
直接创建
new XXX
-
反射创建
通过反射创建
-
基本使用
- 引入命名空间:using System.Reflection
- 使用程序集:Assembly (程序集分为exe(可执行文件)、dll(类库))
- 反射结果为接口类型,实现解耦
6.2 实现反射
示例代码1
-
Car 接口
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.InterfaceReflection._1.接口反射 { public interface ICar { string Name { get; set; } // 定义属性 string Description { get; set; } // 定义属性 void Run(); // 定义方法 } }
-
Audi接口实现类
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Remoting.Messaging; using System.Text; using System.Threading.Tasks; namespace ZJF.InterfaceReflection._1.接口反射 { public class AudiImpl : ICar { private string _name; private string _description; public string Name { get => this._name; set => this._name=value; } public string Description { get => this._description; set => this._description=value; } public Audi() { } // 无参构造方法 public Audi(string name, string des) { // 有参构造方法 this._name = name; this._description = des; } public void Run() { Console.WriteLine($"{this.Name} is Running!!!"); // 实现Run方法 } } }
-
使用反射创建对象
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; // 【1】 添加引用 using System.Reflection; using ZJF.InterfaceReflection._1.接口反射; namespace ZJF.InterfaceReflection { internal class Program { static void Main(string[] args) { // 【4】 测试 Test1(); Console.ReadLine(); } static void Test1() { // 【2】 使用new创建模块 Car audi = new Audi("奥迪", "good car"); audi.Run(); try { // 【3】 使用反射创建模块 // 【3.1】Load("程序集").CreateInstance("全连接类名称") var tag = (Car)Assembly.Load("ZJF.InterfaceReflection").CreateInstance("ZJF.InterfaceReflection._1.接口反射.Audi"); tag.Name = "奥迪111"; tag.Description = "good car !!!!!"; tag.Run(); } catch (Exception e) { Console.WriteLine(e); } } } }
示例代码2
将程序集和完整类名通过配置文件提供给用户,提供很好的扩展性。
-
定义一下配置文件App.config
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" /> </startup> <appSettings> <add key="assName" value="ZJF.InterfaceReflection"/> // 程序集可以是可执行文件,也可以是dll文件 <add key="className" value="ZJF.InterfaceReflection._1.接口反射.Audi"/> </appSettings> </configuration>
-
定义对象工厂类ObjectFactory.cs
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Configuration; using ZJF.InterfaceReflection._1.接口反射; namespace ZJF.InterfaceReflection._2.反射工厂类 { public class ObjectFactory { // 【1】 定义程序集名称 private static string assName = ConfigurationManager.AppSettings["assName"]; public static Car GetObject() { // 【2】 定义类名 string className = ConfigurationManager.AppSettings["className"]; // 【3】 返回程序集对象 return (Car)Assembly.Load(assName).CreateInstance(className); } } }
-
调用
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Reflection; using ZJF.InterfaceReflection._1.接口反射; using ZJF.InterfaceReflection._2.反射工厂类; namespace ZJF.InterfaceReflection { internal class Program { static void Main(string[] args) { //Test1(); Test2(); Console.ReadLine(); } static void Test2() { var obj = ObjectFactory.GetObject(); obj.Name = "奥迪汽车"; obj.Run(); } } }
示例代码三
通过反射来实现扩展
7 泛型
1、回顾前面:泛型集合List、Dictionary<k,v>
所在命名空间:System.Collections.Generic;
2、泛型最大的好处:定义的时候,不明确具体的类型,谁使用,谁明确类型。让某一个方法,或者某一个类更具有通用性。
3、泛型概念:它是一种程序特性,泛化(将具体的,扩展为一般的)方法参数的类型。
4、泛型使用:泛型集合、泛型方法、泛型类、泛型委托…
5、泛型延伸:为了后面学习高级委托、多线程做准备。
泛型最大的好处就是:在定义的时候不明确类型,在使用的时候明确类型,让程序具有很好的泛化性,提高程序的通用性
7.1 List和Dictionary回顾
List是一个容器,容器内可以存储任意类型的变量,可以是基本类型也可以是引用类型。
List的用法
-
先准备一个实体学生类:Student.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.GenericTech._01_回顾List和Dictionary { public class Student { // 学生ID public int SId { get; set; } // 姓名 public string Name { get; set; } // 年龄 public int Age { get; set; } public Student() { } public Student(int sId, string name, int age) { SId = sId; Name = name; Age = age; } public void Info() { Console.WriteLine($"同学:{this.Name} 今年 {this.Age}岁了!"); } } }
-
创建一个测试类TestListDict.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.GenericTech._01_回顾List和Dictionary { public class TestListDict { public void Test1() { List<string> nameList = new List<string>() { "张三", "李四", "王五" }; Console.WriteLine($"nameList 一共有{nameList.Count}个元素"); foreach (string name in nameList) { Console.WriteLine($"{name}"); } } public void Test2() { List<int> nums = new List<int>(); for (int i = 0; i < 10; i++) { nums.Add(i); } Console.WriteLine($"nums 中一共有{nums.Count}个元素"); foreach (int i in nums) { Console.WriteLine(i); } } public void Test3() { List<Student> students= new List<Student>() { new Student(1, "张三", 18), new Student(2, "李四", 19), new Student(3, "王五", 20), new Student(4, "马六", 21) }; Console.WriteLine($"总共有:{students.Count}位同学"); foreach (Student student in students) { student.Info(); } } } }
Test1方法中:List中存储的类型是string
Test2方法中:List中存储的类型是int
Test3方法中:List中存储的是实体类Student,List中常用的就是类型
-
调用测试类:Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ZJF.GenericTech._01_回顾List和Dictionary; namespace ZJF.GenericTech { internal class Program { static void Main(string[] args) { Test1(); Console.Read(); } static void Test1() { // 实例化 TestListDict tt = new TestListDict(); tt.Test1(); // 调用方法1 tt.Test2(); tt.Test3(); } } }
-
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8l63xWR-1684052960422)(D:\markdown\C#系列\CSharp高级进阶.assets\image-20230513105431066.png)]
Dictonary用法
Dictionary是一对Key Value ,一个key对应一个value
-
字典用法
public void Test4() { Dictionary<string, string> books = new Dictionary<string, string>() { { "罗贯中","《三国演义》"}, { "施耐庵","《西游记》"}, { "曹雪芹", "《红楼梦》"}, { "吴承恩", "《水浒传》"} }; Console.WriteLine($"一共有{books.Count}本书;"); foreach (string author in books.Keys) { Console.WriteLine($"{author}:{books[author]}"); } }
7.2 引入泛型
我们需要一个计算器,这个计算器需要整数运算和浮点数运算,我们先不用泛型实现这个类:
-
Calc.cs
通过重载来支持不同的数据类型
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace ZJF.GenericTech._02_引入泛型
{
public class Calc
{
static int Add(int x, int y) {
return x + y;
}
static float Add(float x, float y) {
return x + y;
}
static int Sub(int x, int y) {
return x - y;
}
static float Sub(float x, float y) {
return x - y;
}
}
}
这种写法需要写很多多余的代码。
7.3 泛型类、泛型方法、泛型返回值
定义泛型类:在定义的时候不需要明确类型,在使用的时候在明确类型。提高类的通用性。
定义一个泛型类
-
Stack.cs
创建一个栈类,栈是一个后进先出的数据结构
namespace ZJF.GenericTech._03_泛型基础 { // 泛型类 internal class Stack<T> { // 私有成员变量,用于存储元素 private T[] storage; // 私有成员变量,用于记录索引 private int index; // 私有成员变量,用于确定容量 private int capacity=10; public Stack() { this.storage = new T[10]; this.index = 0; } // 压栈:泛型方法 public void Push(T item) { if (this.storage.Length < this.capacity) { this.storage[index] = item; this.index++; } else { throw new Exception("stack is full"); } } // 弹出:泛型返回值 public T Pop() { if (this.storage.Length <= 0) { T temp = this.storage[index - 1]; this.index--; return temp; } else { throw new Exception("空"); } } } }
-
测试类:
static void Test2() { Stack<int> intStack = new Stack<int>(); intStack.Push(1); intStack.Push(2); intStack.Push(3); intStack.Push(4); intStack.Push(5); Console.WriteLine(intStack.Pop()); Console.WriteLine(intStack.Pop()); Console.WriteLine(intStack.Pop()); Console.WriteLine(intStack.Pop()); Console.WriteLine(intStack.Pop()); }
-
返回结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W5tWabcF-1684052960422)(D:\markdown\C#系列\CSharp高级进阶.assets\image-20230513114809677.png)]
代码运行结果与预期是相同的,最后被压栈的元素,最先一个被弹出。
7.4 default关键字
public class MyGeneric<T1, T2>
{
private T1 t1;
private T2 t2;
public MyGeneric() {
t1 = default(T1);
t2 = default(T2);
}
public void Info() {
Console.WriteLine($"{this.t1}-{this.t2}");
}
}
如上面代码所示:一个泛型类,需要在构造方法中对两个私有变量进行初始化,值类型的默认值是0,引用类型的默认值是null
public class MyGeneric<T1, T2>
{
private T1 t1;
private T2 t2;
public MyGeneric() {
t1 = 0;
t2 = null;
}
public void Info() {
Console.WriteLine($"{this.t1}-{this.t2}");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dWtd312b-1684052960423)(D:\markdown\C#系列\CSharp高级进阶.assets\image-20230513120926770.png)]
vs 也很智能的给了提醒,因为在编译阶段并不知道t1和t2的类型,需要到运行阶段才能知道,default关键字的作用就是用使用时类型的初始值赋值给这个属性。
7.5 dynamic关键字
dynamic关键字让c#在编译阶段不检查变量的类型
7.6 泛型约束
-
值类型约束
where T : struct 表示类型参数必须是不可为 null 的值类型。
// 值类型约束 public class MyGenericList<T> where T : struct { // 类成员... }
由于所有值类型都具有可访问的无参数构造函数,因此 struct 约束也表示 new() 约束,所以 struct 约束不能与 new() 约束和 unmanaged 约束一起使用。
-
引用类型约束
where T : class 表示类型参数必须是的引用类型。
// 引用类型约束 public class MyGenericList<T> where T : class { // 类成员... }
此约束还应用于任何类、接口、委托或数组类型。 在可为 null 的上下文中,T 必须是不可为 null 的引用类型。
-
可为空引用类型约束
where T : class? 表示类型参数必须是可为 null 或不可为 null 的引用类型。
// 引用类型约束 public class MyGenericList<T> where T : class? { // 类成员... }
此约束还应用于任何类、接口、委托或数组类型。
-
不可为空约束
where T : notnull 表示类型参数必须是不可为 null 的类型。
// 引用类型约束 public class MyGenericList<T> where T : notnull { // 类成员... }
参数可以是不可为 null 的引用类型,也可以是不可为 null 的值类型。
-
基方法约束
where T : default 表示重写方法或提供显式接口实现时,如果需要指定不受约束的类型参数,此约束可解决歧义。
// 引用类型约束 public class MyGenericList<T> where T : default { // 类成员... }
default 约束表示基方法,但不包含 class 或 struct 约束。
-
非托管类型约束
where T : unmanaged 表示类型参数必须是不可为 null 的非托管类型。
// 引用类型约束 public class MyGenericList<T> where T : unmanaged { // 类成员... }
unmanaged 约束也表示 struct 约束,且不能与 struct 约束或 new() 约束结合使用。
-
构造函数约束
where T : new() 表示类型参数必须具有公共无参数构造函数。
// 引用类型约束 public class MyGenericList<T> where T : new() { // 类成员... }
当构造函数约束与其他约束一起使用时,new() 约束必须最后指定。 new() 约束不能与 struct 和 unmanaged 约束结合使用。
-
基类名约束
where T :<基类名> 表示类型参数必须是指定的基类或派生自指定的基类。
// 引用类型约束 public class MyGenericList<T> where T :<基类名> { // 类成员... }
在可为 null 的上下文中,T 必须是从指定基类派生的不可为 null 的引用类型。
-
可为空的基类名约束
where T :<基类名>? 表示类型参数必须是指定的基类或派生自指定的基类。
// 引用类型约束 public class MyGenericList<T> where T :<基类名>? { // 类成员... }
在可为 null 的上下文中,T 可以是从指定基类派生的可为 null 或不可为 null 的类型。
-
接口类型约束
where T :<接口名称> 表示类型参数必须是指定的接口或实现指定的接口。
// 引用类型约束 public class MyGenericList<T> where T :<接口名称> { // 类成员... }
可指定多个接口约束。 约束接口也可以是泛型。 在的可为 null 的上下文中,T 必须是实现指定接口的不可为 null 的类型。
-
可为空接口类型约束
where T :<接口名称>? 表示类型参数必须是指定的接口或实现指定的接口。
// 引用类型约束 public class MyGenericList<T> where T :<接口名称>? { // 类成员... }
可指定多个接口约束。 约束接口也可以是泛型。 在可为 null 的上下文中,T 可以是可为 null 的引用类型、不可为 null 的引用类型或值类型。 T 不能是可为 null 的值类型。
-
自定义约束
where T : U 表示为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。
// 引用类型约束 public class MyGenericList<T> where T : U { // 类成员... }
在可为 null 的上下文中,如果 U 是不可为 null 的引用类型,T 必须是不可为 null 的引用类型。 如果 U 是可为 null 的引用类型,则 T 可以是可为 null 的引用类型,也可以是不可为 null 的引用类型。
8 委托
8.1 引入委托
假设有两个类,一个类是AA,AA类中aa方法,一个类是BB,BB类中有bb方法,如果在AA类中调用bb方法,可以在BB类中定义一个属性位AA类的对象,但是如果在BB类中调用调用aa方法就很难实现了,这就是委托的应用场景。
委托相当于中介,我们通过中介找到房东,没有中介我们就找不到房东。
-
AA.cs
public class AA { public void aa() { Console.WriteLine("我是AA类的aa方法"); } }
-
BB.cs
public class BB { public AA Aa { get; set; } public BB(AA a) { Aa = a; } public void bb() { Aa.aa(); Console.WriteLine("我是BB类的bb方法"); } }
-
Program.cs
public class Program { static void Main(string[] args) { Test1(); Console.Read(); } static void Test1() { AA a = new AA(); BB b = new BB(a); b.bb();} }
8.2 使用委托的步骤
【1】声明委托类型(定义方法的原型),一般委托都要声明在类的外面,委托本身也是一种特殊的数据类型。
【2】编写具体方法(符合方法的原型)
【3】创建委托变量
【4】关联委托方法(将某一个或多个具体的方法和一个委托变量关联)
【5】使用委托变量(调用所关联的方法)
多路委托:一个委托变量关联了若干方法,当调用委托变量的时候,所关联的方法会按照关联的顺序依次执行。
-
Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ZJF.DelegateTest._01_引入委托; using ZJF.DelegateTest._02_委托使用方法; namespace ZJF.DelegateTest { internal class Program { static void Main(string[] args) { Test2(); Console.Read(); } static void Test2() { DelegateTest1 dt1 = new DelegateTest1(); //【3】创建委托变量 SayHi sayHi = new SayHi(dt1.TestSayHi); //【4】关联委托方法(将某一个或多个具体的方法和一个委托变量关联) sayHi += dt1.TestSayHello; //【5】使用委托变量(调用所关联的方法) sayHi(); } } // 【1】声明委托类型(定义方法的原型),一般委托都要声明在类的外面,委托本身也是一种特殊的数据类型。 public delegate void SayHi(); }
-
DelegateTest.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.DelegateTest._02_委托使用方法 { public class DelegateTest1 { // 委托原型:public delegate void sayHi() // 委托实现:public void TestSayHi() //【2】编写具体方法(符合方法的原型) public void TestSayHi() { Console.WriteLine("hello1"); } public void TestSayHello() { Console.WriteLine("hello2"); } } }
8.3 委托的基本应用
窗体之间的逆向传值
实现窗体之间的逆向传值,父窗体可以轻松把值交给子窗体,子窗体想要把值交给父窗体,那就需要使用委托了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-13mj6Lm8-1684052960423)(D:\markdown\C#系列\CSharp高级进阶.assets\image-20230514151417074.png)]
-
FrmChild.cs
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace ZJF.DelegateWindowTest { public partial class FrmChild : Form { public FrmChild() { InitializeComponent(); } public TransferMsg SendMsg { get; set; } private void button1_Click(object sender, EventArgs e) { // 【5】调用委托变量 SendMsg(this.childMsg.Text); } } }
-
FrmMain.cs
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace ZJF.DelegateWindowTest { public partial class FrmMain : Form { public FrmMain() { InitializeComponent(); } // 【2】编写具体方法(准备逆向调用的方法,和声明委托在同一个类中) public void RecvivedMsg(string value) { this.mainMsg.Text += value + "\r\n"; } private void button1_Click(object sender, EventArgs e) { for (int i = 0; i < 3; i++) { FrmChild frm = new FrmChild(); frm.Show(); // 【3】这一步很关键,将委托变量与执行函数进行了关联。 frm.SendMsg += RecvivedMsg; } } } //【1】声明委托类型(在哪个类中都可以,建议调用哪个方法,就在哪个类中) public delegate void TransferMsg(string value); }
-
梳理一下逻辑
- 子窗体需要通过委托调用父窗体中的接收信息方法
- 调用方法同一个类中定义委托
- 子窗体中通过属性保存委托变量
- 调用属性进而调用委托后面的方法
父窗体将信息广播给子窗体
通过多路委托实现,多路委托能够很好的实现广播机制。
8.4 匿名方法
语法
-
基础语法
delegate (params){ // 方法体 }
使用delegate关键字创建匿名方法,这个匿名方法可以用于直接关联委托变量。
-
一般的委托用法
namespace ZJF.Anonymous { public class Program { public static void Main(string[] args) { Test1(); Console.Read(); } // 【3】委托背后的方法 public static int AddCalc(int a, int b) { return a + b; } public static void Test1() { // 【2】 关联委托 Add addDelegate = new Add(AddCalc); Console.WriteLine( addDelegate(1,2) ); } } //【1】定义一个委托 public delegate int Add(int x, int y); }
这里面需要注意的是,委托关联的函数都是需要定义的,我们使用匿名方法来简化这个程序、
-
使用匿名方法简化定义
namespace ZJF.Anonymous { public class Program { public static void Main(string[] args) { Test1(); Console.Read(); } //public static int AddCalc(int a, int b) { // return a + b; //} public static void Test1() { // 通过匿名方法直接关联委托变量,简化操作 Add addDelegate = delegate(int x, int y) { return x + y; }; Console.WriteLine( addDelegate(1,2) ); } } public delegate int Add(int x, int y); }
匿名方法好处:将具体方法和委托直接关联到一起,如果委托只需要一个方法的时候,匿名方法肯定是显得简单。
8.5 lambda表达式
Lambda表达式:在C#3.0出现的。使用这种表达的可以更简练的编写代码块。
CalculatorDelegate cal3 = (int a, int b) => { return a + b; };
CalculatorDelegate cal4 = (a, b) => a - b;
【1】在Lambda表达式中参数类型可以是明确类型,也可以是推断类型。
【2】如果是推断类型,则参数类型可以由编译根据上下文自动推断出来。
【3】运算符=> 读作goes to ,运算符左边输入参数(如果有),右边是表达式或语句块。
【4】表达式两种方式:
(input args)=>表达式 # 单一表达式
(input args)=>{ 语句1;语句2;语句3....} # 多条语句
【5】Lambda表达式和匿名方法的比较
第一、Lambda表达式本身就是匿名方法。
第二、Lambda表达式允许不指明参数类型,但是匿名方法必须要明确。
第三、Lambda表达式允许单一的表达式或多条语句组成,而匿名方法不允许单一表达式。
-
测试
namespace ZJF.Anonymous { public class Program { public static void Main(string[] args) { Test2(); Console.Read(); } //public static int AddCalc(int a, int b) { // return a + b; //} public static void Test1() { Add addDelegate = delegate(int x, int y) { return x + y; }; Console.WriteLine( addDelegate(1,2) ); } public static void Test2() { Add addDelegate = (a, b) => { return a + b; }; // 使用lambda表达式更简单 Console.WriteLine(addDelegate(1,2)); } } public delegate int Add(int x, int y); }
8.6 自定义泛型委托
1、为什要使用泛型委托?
普通委托在数据类型的限定上非常严格的。有时候我们需求变化,可能适应不了。
2、泛型委托定义:本质上和泛型方法是非常相似的,泛型委托关联的时候,可以是具体方法、匿名方法、也可以是lambda表达式。
-
MyGenericDelegate.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ZJF.Anonymous._01_自定义泛型委托 { public class MyGenericDelegate { public static void Test1() { //【1】使用泛型委托关联具体方法 CalculateAdd<int> calc = new CalculateAdd<int>(Add); Console.WriteLine(calc(1, 2)); CalculateAdd<double> calc2 = new CalculateAdd<double>(Sub); Console.WriteLine(calc2(1.0, 2)); // 【2】使用泛型委托关联匿名方法 CalculateAdd<int> calc3 = delegate (int x, int y) { return x + y; }; CalculateAdd<double> calc4 = delegate (double x, double y) { return x - y; }; Console.WriteLine(calc3(1, 2)); Console.WriteLine(calc4(1.0, 2)); // 【3】 使用泛型委托关联lambda表达式 CalculateAdd<int> calc5 = (x, y) => { return x + y; }; CalculateAdd<double> calc6 = (x, y) => { return x - y; }; Console.WriteLine(calc5(1, 2)); Console.WriteLine(calc6(1.0, 2)); } public static int Add(int x, int y) { return x + y; } public static double Sub(double x, double y) { return x - y; } } // 【1】自定义泛型委托 public delegate T CalculateAdd<T>(T x, T y); }