基本结构
using System; // 引入其它程序集
namespace CSharpLearn // 命名空间,通常为当前程序集名称
{
class Program // 类
{
/// <summary>
/// 函数注释
/// </summary>
/// <param name="变量名称">变量解释</param>
static void Main(string[] args) // 入口函数
{
#region VarDefine // #region 折叠代码块名称
string outStr = "Hello C#!"; // 变量定义
const int id = Int32.MaxValue; // 常量定义
#endregion // #endregion 结束折叠代码块
#region MainFunc
try // 异常捕获
{
Console.WriteLine(outStr);
}
catch (Exception e)
{
Console.WriteLine(e);
throw; // 抛出异常
}
finally
{
}
#endregion
}
}
}
常用用法
1.字符串拼接
string str = string.Format("内容{0}内容{1}内容{2}内容{...}",0位置的值,1位置的值,2位置的值,...);
string str = $"内容{0位置的值}内容{1位置的值}内容{2位置的值}内容{...}";
-----------------------------------------------
// 需要频繁操作字符串,可以使用StringBuilder
using System.Text;
// 初始化
StringBuilder stringBuilder = new StringBuilder("1233bcfh7ds");
stringBuilder.Append(" abc"); // 附加
stringBuilder.Insert(3, "VVV"); // 插入
stringBuilder.Remove(6, 3); // 移除
stringBuilder.Replace("VVV", "AAA"); // 替换
stringBuilder[0] = 'Q'; // 访问/修改
Console.WriteLine(stringBuilder); // Q23AAAfh7ds abc
stringBuilder.Clear(); // 清空
2. 枚举
enum EMyEnum // 枚举名称
{
Name0, // 0
Name1 = 5,
Name2, // 6
Name3 = 100,
Name4, // 101
Name5 // 102
}
string str = myEnum.ToString(); // 枚举转字符串
myEnum = (EMyEnum)Enum.Parse(typeof(EMyEnum), "Name5"); // 字符串转枚举
3.数组
// 一维数组声明
int[] arr;
// 一维数组初始化
int[] arr = new arr[5];
int[] arr = new arr[5]{1,2,3,4,5};
int[] arr = new arr[]{1,2,3,4,5,6,7,8};
int[] arr = {1,2,3,4,5,6};
---------------------------------------------------------
// 二维数组声明
int[,] arr;
// 二维数组初始化
int[,] arr = new arr[3,3];
int[,] arr = new arr[3,3]{{1,2,3},
{4,5,6},
{7,8,9}};
int[,] arr = new arr[,]{{1,2,3},
{4,5,6},
{7,8,9}};
int[,] arr = {{1,2,3},
{4,5,6},
{7,8,9}};
交错数组
基本语法
// 交错数组声明
// 变量类型[][] 变量名称 = new 变量类型[行数][];
int[][] arr;
// 交错数组初始化
// 变量类型[][] 变量名称 = new 变量类型[行数][]{一维数组1,一维数组2,一维数组3,...};
int[][] arr = new int[6][];
// 一维数组的类型必须与交错数组定义的一致
int[][] arr = new int[3][]{new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 7, 8, 9, 10 }};
int[][] arr = new int[][]{new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 7, 8, 9, 10 }};
int[][] arr = {new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 7, 8, 9, 10 }};
用法
1. 数组长度
// 获取交错数组行数
arr.GetLength(行数);
// 获取某一行一维数组的列数
arr[行数].Length;
2. 获取元素
// 获取一维数组
arr[行数];
// 获取一维数组中的元素
arr[行数][索引值];
4. 值和引用类型
值类型
不同的值类型变量拥有独立的内存空间,互不干扰(栈空间)
数值类型、结构体、bool型、枚举、可空类型
int a = 10;
int b = a; // b=10,a=10
b = 30; // b=30,a=10
// b并不是指向a,而是将a指向的值拷贝到b指向的内存空间中
// 所以修改b,a没有变化
引用类型
将b变量指向a变量的地址,通过b改变值,a指向的值也会改变,相当于b是a的别名(堆空间)
数组、委托、接口、object、string(特殊引用类型)、类
int[] a = {1,2,3,4};
int[] b = a;
b[0] = 33; // a{33,2,3,4} b{33,2,3,4}
// b指向a,a又指向值
// 所以修改b,a会变化
特殊string引用类型
// 当string重新赋值时,会重新分配内存空间
string str = "123";
string str_c = str;
str_c = "666"; // 相当于:str_c = new string("666");
函数修饰符ref和out
共同点:都是修饰传入的参数为引用类型,使得函数内部改变值,实际的值也会改变
不同点:ref
传入的变量必须初始化;out
传入的变量必须在函数内部赋值
int a = 20;
// ref
static void ChangeValue(ref int num)
{
num = 333;
}
ChangeValue(ref a);
-----------------------------------------
// out
static void UnchangeValue(out int num)
{
num = 666;
}
UnchangeValue(out a);
5. 变长参数和参数默认值
变长参数只能为参数列表的最后一个
// params 参数类型[] 参数名
static int SumNum(int a, params int[] arr)
{
return a + arr.Sum();
}
SumNum(66, 1, 2, 3, 4, 5, 6);
参数默认值只能在普通参数的后面
static void Say(double price, string str="book", int num=99)
{
}
Say(.45, str:"card"); // 可以不传入已经有默认值的参数
面向对象(核心)
1. 三大特性
封装
用程序语言形容对象
class MyClass
{
}
构造函数
默认有无参构造。如果写了有参构造,没写无参构造,那默认的无参构造就会消失
class MyClass
{
private int id;
private string name;
public MyClass()
{
id = 0;
}
public MyClass(string name):this() // this(参数) 调用其它构造函数
{
this.name = name;
}
public MyClass(int id):this("oop")
{
this.id = id;
}
}
内存回收(GC)
一般在游戏进度条加载时候进行,强制回收会消耗性能,可能会有卡顿
// 强制执行内存回收
GC.Collect();
成员属性
get/set访问器,二者可选其一或都用,可以使成员变量只能读
/只能写
/能读能写
可以在set中写加密逻辑,get中写解密逻辑,用以保护成员变量
public class Person
{
private int id;
private string name;
private bool sex;
public string Name
{
get // 可以加访问修饰符,默认是属性的修饰符
{
string newName = name + id;
return newName;
}
set
{
string newName = value + "ok";
name = newName;
}
}
// 自动属性,不用进行特殊处理可以这样写
public int Id
{
get;
set;
}
}
-----------------------------------------
OOP.Person person = new OOP.Person();
person.Name = "none";
person.Id = 223;
Console.WriteLine($"{person.Name} --- {person.Id}");
索引器
让类对象可以像数组一样通过索引访问其中的元素
//访问修饰符 返回值 this[参数类型 参数名,参数类型 参数名,...]
//{
// get{}
// set{}
//}
public class Person
{
private int id;
private string name;
private bool sex;
private Person[] friends;
public Person this[int index]
{
get
{
return _friends[index];
}
set
{
_friends[index] = value;
}
}
}
------------------------------------
Person person = new OOP.Person();
Person p = person[0]; // 通过索引器访问成员变量
静态构造函数
1.只会自动调用一次;
2.常用来初始化静态变量;
3.可以写在静态和普通函数中;
public class Person
{
static Person()
{
}
}
扩展方法
可以为现有非静态变量类型添加新方法
只能在非嵌套静态类中定义
如果定义的扩展方法及参数类型与原有方法一致,则默认调用原方法
// 访问修饰符 static 返回值 函数名(this 被扩展的类名 参数名,参数类型 参数名,参数类型 参数名,...)
static class Tool
{
public static void PrintInt(this int value)
{
Console.WriteLine("Int value = " + value);
}
}
-----------------
114.PrintInt(); // Int value = 114
运算符重载
使自定义类和结构体对象进行运算
1.不能使用ref和out
2.条件运算符需要成对重载。如重载了<,则必须重载>
3.&&
,||
,[]
,()
,.
,=
,?:
不能重载
访问修饰符 static 返回值类型 operator 运算符(参数列表){}
public class Point
{
private int _x;
private int _y;
public Point()
{
}
public Point(int x, int y)
{
this._x = x;
this._y = y;
}
// 重写输出方法
public override string ToString()
{
return $"({_x},{_y})";
}
// 重载加法+运算符
public static Point operator +(Point p1, Point p2)
{
return new Point(p1._x + p2._x, p1._y + p2._y);
}
}
---------------------------------
Point p1 = new Point(1, 2);
Point p2 = new Point(5, 3);
Console.WriteLine(p1 + p2); // (6,5)
继承
复用封装对象的代码(子类继承父类)
1.单根性:子类只能有一个父类
2.传递性:子类可以间接继承父类的父类
class 子类名 : 父类名 {}
class Teacher
{
}
class TeachTeacher : Teacher
{
}
构造函数
1.子类创建时,先执行父类的构造函数,再执行子类的构造函数
2.父类的无参构造消失后,子类要用base显示调用指定父类构造函数
class Father
{
public Father(int num)
{
}
}
class Son : Father
{
// 子类要用base显示调用指定父类构造函数
public Son(int id) : base(id)
{
}
}
装箱和拆箱
装箱:值类型用引用类型存储。栈内存 -> 堆内存
object v = 3;
拆箱:将值类型从引用类型取出来。堆内存 -> 栈内存
int value = (int)v;
示例:
static void Func(params object[] arr) {}
Func("123",1,24,.45f,.88877,new Son());
密封类(sealed)
sealed修饰的类不能再被继承
sealed class MyClass {}
多态
同样行为的不同表现(子类继承父类,子类有父类没有的行为,相当于扩展父类)
为了使同一个对象有唯一的行为
问题描述
class Father
{
public void Speak()
{
Console.WriteLine("Father");
}
}
class Son : Father
{
public new void Speak()
{
Console.WriteLine("Son");
}
}
--------------------------------------
Father son = new Son();
son.Speak(); // 实例化是用子类实例化,但是直接调用确实父类的函数
(son as Son)?.Speak(); // 需要转换才能调用子类的函数
可以看到,son可以调用有2种相同的函数,Father的Speak和Son的Speak
Father son = new Son();
son.Speak();
(son as Son)?.Speak();
为了使son只能调用自己的Speak函数,就要用到多态(即对象new的是什么类,就只能执行什么类里的函数)
需要用到重载,virtual(虚函数),override(重写),base(父类)
以下是解决方法:
解决问题
class GameObject
{
protected string Name;
public GameObject(string name)
{
Name = name;
}
// 父类定义虚函数,子类可以重写这个函数
public virtual void Atk()
{
Console.WriteLine($"GameObject \"{Name}\" attack.");
}
}
class Player : GameObject
{
public Player(string name) : base(name)
{
}
// 子类可以重写virtual虚函数
public override void Atk()
{
// 可以通过base.调用父类的函数
// base.Atk();
Console.WriteLine($"Player \"{Name}\" attack.");
}
}
--------------------------------
GameObject player = new Player("fu");
player.Atk(); // 父类对象装载子类的实例,调用子类重写的函数
抽象函数
1.子类直接继承后必须实现;
2.必须定义在抽象类中;
3.修饰符只能是public
或protected
;
// 定义
abstract class Thing
{
public abstract void Say(); // 又叫 纯虚方法
}
// 继承
class Apple : Thing
{
public override void Say()
{
Console.WriteLine("Apple.");
}
}
接口
接口是行为的抽象规范(相当于将某些特定行为抽象出来,需要用到的类继承接口后,就有了相应的行为)
1.只能包含方法
、属性
、索引器
、事件
;
2.类继承接口后必须实现接口中的所有成员;
3.类可以继承多个接口;
4.接口不能继承类,可以继承另一个接口;
5.类继承接口后,实现的成员必须是public
;
6.接口也遵循里氏替换原则;
语法
访问修饰符 interface I接口名 {}
定义
public interface IFly
{
// 方法
void Fly();
// 属性
string Name
{
get;
set;
}
// 索引器
string this[int index]
{
get;
set;
}
// 事件
event Action DoSomething;
}
使用
class Person : Animal, IFly
{
// 类继承接口后,实现的成员必须是public
public virtual void Fly() // 加上virtual后,可以让继承这个类的子类去重写
{
}
public string Name { get; set; }
public string this[int index]
{
get
{
return "";
}
set
{
}
}
public event Action? DoSomething;
}
------------------------
IFly f = new Person(); // f只能调用Person已经实现IFly的成员,不能调用Person里的其它成员
显示实现接口
当一个类同时继承2个接口,且2个接口有同种方法时使用
1.不能使用修饰符;
返回值类型 接口名.方法名() {}
interface IAtk
{
public void Atk();
}
interface ISuperAtk
{
public void Atk();
}
class MyPlayer : IAtk, ISuperAtk
{
// 显示实现接口
void IAtk.Atk()
{
}
void ISuperAtk.Atk()
{
}
}
---------------------------
IAtk ia = new MyPlayer();
ISuperAtk isa = new MyPlayer();
ia.Atk(); // 调用IAtk接口的Atk方法(Atk方法由MyPlayer实现)
isa.Atk(); // 调用ISuperAtk接口的Atk方法(Atk方法由MyPlayer实现)
但是显示实现后,MyPlayer
的对象不能直接调用Atk
方法,必须要先转换成对应的接口
MyPlayer player = new MyPlayer();
(player as IAtk).Atk();
(player as ISuperAtk).Atk();
密封函数
使函数不能再被重写
public sealed override void Atk() {} // 后续继承该类的类不能再重写此Atk函数
2.七大原则
详细可以看我写的
里氏替换原则
概念
任何父类出现的地方,子类都可以替代
语法表现
父类的容器可以装载子类对象,因为子类对象包含了父类所有内容
public static void Run()
{
// 使用 父类对象 装载 子类对象
GameObject player = new Player();
GameObject monster = new Monster();
GameObject boss = new Boss();
}
作用
方便进行对象存储和管理
代码定义
class GameObject
{
}
class Player : GameObject
{
public void PlayerAtk(double damage)
{
Console.WriteLine($"Player attack {damage} damage.");
}
}
class Monster : GameObject
{
public void MonsterAtk(double damage)
{
Console.WriteLine($"Monster attack {damage} damage.");
}
}
class Boss : GameObject
{
public void BossAtk(double damage)
{
Console.WriteLine($"Boss attack {damage} damage.");
}
}
is和as
is:用来判断一个对象是否为指定对象
as:将一个对象转换成指定对象的类
GameObject player = new Player();
// 判断player是否为Player类的对象
if (player is Player)
{
// 将GameObject类的变量player转换为Player类的变量
(player as Player)?.PlayerAtk(.114); // ?代表如果转换结果为null,则不执行.后面的代码
}
开闭原则
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
即,对系统进行抽象约束
依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象
单一职责原则
一个类只负责一项职责。换种说法,就一个类而言,应该只有一个引起它变化的原因
接口隔离原则
类的依赖关系应建立在最小接口上,不要都塞在一起。即类不应该依赖它不需要的接口
合成复用原则
合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用
迪米特原则
一个对象应尽可能少的了解其它对象,尽量降低类与类之间的耦合
数据集合
ArrayList
本质是object的数组
using System.Collections;
ArrayList list = new ArrayList();
可以添加任意的类型:
list.Add("ad");
list.Add(null);
list.Add(9);
// 将一个数组添加到另一个最后
list.AddRange(new ArrayList { 123, "fu" });
list.AddRange(new int[]{1,2,3});
删改查:
list.Remove(1);
list.RemoveAt(2);
list.Clear();
list[0];
装箱和拆箱:
// 装箱
int i = 1;
list[0] = i;
// 拆箱
i = (int)list[0];
Stack
本质为object数组
Stack
是栈存储容器,遵循先进后出原则
先进入的数据后存取,后进入的数据先存取
using System.Collections;
Stack stack = new Stack();
stack.Push("777"); // 增加(压栈)
stack.Push(0x56);
stack.Pop(); // 取(弹栈),取出的值已经不在里面了
Console.WriteLine(stack.Pop());
stack.Peek(); // 仅查看,不删除
stack.Contains("777"); // 是否包含在栈中
stack.Count(); // 长度
stack.ToArray(); // 转化为object数组
Queue
本质为object数组
Queue
是队列存储容器,遵循先出后进原则
先进入的数据先存取,后进入的数据后存取
using System.Collections;
Queue queue = new Queue();
// 新增元素
queue.Enqueue(122);
queue.Enqueue("opp");
queue.Enqueue(.78f);
queue.Dequeue(); // 取出元素
queue.Peek(); // 查看元素
queue.Contains(.78f); // 是否包含
queue.Clear(); // 清空
queue.Count(); // 长度
queue.ToArray(); // 转化为object数组
Hashtable
又称散列表,是由键的哈希代码组织起来的键值对
主要用于提高数据查询效率
使用键访问元素
不能出现相同键;不能修改键,只能修改键对应的值
using System.Collections;
Hashtable hashtable = new Hashtable();
hashtable.Add(1, "abc"); // 增加键值对
hashtable.Add("good", .345);
hashtable.Remove(1); // 通过键删除键值对
hashtable.Clear(); // 清空
var num = hashtable["good"]; // 通过键查找对应值,找不到返回null
// 根据键检测是否包含
hashtable.Contains(1);
hashtable.ContainsKey(1);
// 根据值检测是否包含
hashtable.ContainsValue(.345);
// 通过键修改值,不能修改键
hashtable["good"] = "ikun";
泛型
多个泛型用逗号分隔
class 类名<泛型占位符>
interface 接口名<泛型占位符>
// 泛型函数
修饰符 返回值类型 函数名<泛型占位符>(参数列表) {}
class TestClass<T>
{
public T t;
}
interface ITest<T>
{
T Num
{
get;
set;
}
}
void Func<T>()
{
T t;
}
泛型约束
where 泛型字母:值类型 // interface ITest<T> where T:struct {}
where 泛型字母:引用类型 // void Func<T>() where T:class {}
where 泛型字母:无参公共构造函数 // void Func<T>() where T:new() {}
where 泛型字母:接口名 // void Func<T>() where T:ITest {}
where 泛型字母:另一个泛型字母 // void Func<T,K>() where T:K {}
多约束类型
void Func<T>() where T:class, new() {}
多泛型字母的约束
void Func<T,K>() where T:class, new() where K:struct {}
常用泛型数据结构
// 列表
List<int> list = new List<int> { 1, 2, 3, 4 };
-------------------------
// 字典
Dictionary<int, string> dictionary = new Dictionary<int, string>
{
{ 1, "app" },
{ 2, "cpp" },
{ 1114, "nuk" }
};
List排序
Sort()
List<int> list = new List<int> { 1, 5, 67, 9, 0, 3, 4, 54, 3, 0 };
list.Sort();
自定义类排序
继承接口IComparable
即可进行排序和比较
class Item : IComparable<Item>
{
public int Money { get; set; }
public Item(int money)
{
Money = money;
}
// 重写排序比较逻辑
public int CompareTo(Item? other)
{
// 返回值小于0:this放在传入对象other的前面
// 返回值等于0:this保持当前位置不变
// 返回值大于0:this放在传入对象other的后面
return Money - (other?.Money ?? 0); // 升序
return (other?.Money ?? 0) - Money; // 降序
}
}
--------------------------------------------
List<Item> items = new List<Item>
{
new Item(100),
new Item(223),
new Item(0),
new Item(9999),
new Item(150),
new Item(223)
};
items.Sort(); // 排序
foreach (var item in items)
{
Console.WriteLine(item.Money);
}
重写排序函数返回值
返回值小于0:this放在传入对象other的前面 返回值等于0:this保持当前位置不变 返回值大于0:this放在传入对象other的后面
相当于一个数轴
升序
0
100
150
223
223
9999
降序
9999
223
223
150
100
0
委托排序
使用comparison
委托进行排序
函数返回值与自定义类排序相同,只不过第一个参数item
为this
,第二个参数shopItem
为other
就是第一个参数与第二个参数比较,第一个参数放在第二个参数的前面,后面还是不变
class ShopItem
{
public int Id { get; set; }
public ShopItem(int id)
{
Id = id;
}
}
--------------------------------
List<ShopItem> shopItems = new List<ShopItem>
{
new ShopItem(100),
new ShopItem(223),
new ShopItem(0),
new ShopItem(9999),
new ShopItem(150),
new ShopItem(223)
};
// 使用comparison委托进行排序
// 第一个参数与第二个参数比较,第一个参数放在第二个参数的前面,后面还是不变
shopItems.Sort((item, shopItem) => item.Id - shopItem.Id); // 升序
shopItems.Sort((item, shopItem) => shopItem.Id - item.Id); // 降序
foreach (var item in shopItems)
{
Console.WriteLine(item.Id);
}
可变类型的泛型双向链表
// 创建链表
LinkedList<int> linkedList = new LinkedList<int>();
------------------------------增-------------------------
// 在链表尾部加节点,数据为10
linkedList.AddLast(10);
// 在链表头部加节点,数据为20
linkedList.AddFirst(20);
// 在某一个节点之后添加节点
LinkedListNode<int> n1 = linkedList.Find(20);
linkedList.AddAfter(n1, 30);
// 在某一个节点之前添加节点
LinkedListNode<int> n2 = linkedList.Find(10);
linkedList.AddBefore(n2, -5);
------------------------------删-------------------------
// 移除头节点
linkedList.RemoveFirst();
// 移除尾结点
linkedList.RemoveLast();
// 移除指定值所在的节点
linkedList.Remove(20);
// 清空
linkedList.Clear();
------------------------------查-------------------------
// 获取头节点
LinkedListNode<int> first = linkedList.First;
// 获取尾节点
LinkedListNode<int> last = linkedList.Last;
// 获取指定值的节点(找不到返回null)
LinkedListNode<int> node = linkedList.Find(10);
// 查询是否存在
linkedList.Contains(-5);
------------------------------改-------------------------
// 得到节点,改变节点的值
node.Value = 100;
------------------------------遍历-------------------------
// 遍历
foreach (var item in linkedList)
{
Console.WriteLine(item);
}
// 从头到尾
LinkedListNode<int> nowNode = linkedList.First;
while (nowNode != null)
{
Console.WriteLine(nowNode.Value);
nowNode = nowNode.Next;
}
// 从尾到头
nowNode = linkedList.Last;
while (nowNode != null)
{
Console.WriteLine(nowNode.Value);
nowNode = nowNode.Previous;
}
委托
概念
委托是函数的容器,用来表示函数的变量类型
委托本质是个类,用来定义函数的类型(返回值和参数的类型)
不同的函数必须对应和各自“格式”一致的委托
语法
访问修饰符 delegate 返回值类型 委托名(参数列表);
访问修饰符默认public
声明位置
namespace
(常写)和class
语句块中
定义自定义委托(单播委托)
一个函数变量只存储了一个函数
namespace abc
{
// 声明了一个可以用来存储无参无返回值的函数容器(相当于定义了一个函数变量类)
public delegate void MyFuncVoid();
// 声明了一个可以用来存储int参数int返回值的函数容器
public delegate int MyFuncInt(int num);
public class DelegateLearn
{
static void FuncVoid()
{
Console.WriteLine("FuncVoid");
}
static int FuncInt(int num)
{
Console.WriteLine(num);
return ++num;
}
public static void Run()
{
// 实例化函数变量类,赋值(将函数装入变量中)
MyFuncVoid myFuncVoid = FuncVoid;
// 不同的函数必须对应和各自“格式”一致的委托,“格式”又叫函数签名
MyFuncInt myFuncInt = FuncInt;
// 通过函数变量调用存储的函数
myFuncVoid.Invoke(); // 相当于 FuncVoid();
myFuncVoid(); // 另一种调用方法
int retValue = myFuncInt.Invoke(114);
int retValue2 = myFuncInt(38);
}
}
}
定义自定义委托(多播委托)
一个函数变量存储了多个函数
有返回值的,默认返回最后一个函数执行的返回值
MyFuncVoid myFuncVoid = FuncVoid; // 初始化赋值
myFuncVoid += FuncVoid; // 添加委托
myFuncVoid -= FuncVoid; // 删除委托
// myFuncVoid = null; // 清空委托
myFuncVoid?.Invoke(); // 该函数变量存储了2个相同的函数FuncVoid,调用时会执行2次
MyFuncInt myFuncInt = FuncInt;
myFuncInt += FuncInt;
Console.WriteLine(myFuncInt(112)); // 有返回值的,默认返回最后一个函数执行的返回值
作用
委托当做函数参数传递,在函数内部,先处理一些逻辑,当这些逻辑处理完后,再执行传入的函数。
系统提供的委托
无参无返回值
Action action = FuncVoid;
无参自定义返回值
static string FuncString()
{
return "OK";
}
Func<string> func = FuncString;
有参无返回值(最多16个参数)
static void FuncIntNo(int a, string b) {}
Action<int, string> action = FuncIntNo;
有参有返回值(最多16个参数)
static bool FuncFull(float f, int i, string s)
{
return true;
}
Func<float, int, string, bool> fun = FuncFull;
事件
1.事件基于委托存在;
2.事件是委托的安全包裹,让委托的使用更安全;
3.事件是一种特殊的变量类型;
4.只能做为成员变量存在于类
、接口
和结构体
中;
5.不能在类外部赋值,但是可以增加和删除;
6.不能在类外部调用;
class Test
{
public event Action? MyEvent; // ?表示对象可以为null
void TestFunc()
{
Console.WriteLine("In");
}
public void TestRun()
{
MyEvent += TestFunc;
MyEvent += TestFunc;
MyEvent -= TestFunc;
MyEvent?.Invoke();
}
}
public static void Main(string args)
{
Test test = new Test();
// 不能在类外部赋值,但是可以增加和删除
test.MyEvent += () => { Console.WriteLine("Out"); };
// 不能在外部调用事件
test.TestRun();
}
匿名函数
主要配合委托和事件使用
语法
delegate (参数列表)
{
...
};
用法
public event Action? MyEvent;
public void TestRun()
{
// 无参无返回值
MyEvent += delegate ()
{
Console.WriteLine("匿名函数");
};
// 有参无返回值
Action<int, string> action = delegate(int a, string s)
{
Console.WriteLine($"{a},{s}");
};
// 有参有返回值
Func<int, bool> func = delegate(int a)
{
return a > 20;
};
MyEvent?.Invoke();
action.Invoke(100, "ok");
Console.WriteLine(func.Invoke(23));
}
缺点
添加到委托或事件后,不记录,无法单独移除
lambda表达式
使用方法与匿名函数相同
语法
(参数列表) => { ... }
用法
// 无参无返回
MyEvent += () => { Console.WriteLine("none"); };
// 有参无返回
Action<int, string> action = (a, s) => { Console.WriteLine($"{a},{s}"); };
// 有参有返回
Func<int, bool> func = a => a > 20;
MyEvent?.Invoke();
action.Invoke(100, "ok");
Console.WriteLine(func.Invoke(23));
闭包
内层的函数可以引用包含在它外层的函数的变量,即使外层函数执行已经终止。
该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值
public event Action? MyEvent;
public void TestRun() // 外层函数
{
int value = 10; // 外层函数变量
// 此处就形成了闭包
MyEvent = () =>
{ // 内层函数
Console.WriteLine(value); // 引用外层函数的变量value
};
}
当内层函数结束执行后,改变了外层函数的值(value)的生命周期,使其并不会被自动释放,而是一直存在。
协变(out)和逆变(in)
概念
协变:里氏替换原则,父类可以装子类,所以子类变为父类,如:string
变为object
感受是和谐的
逆变:子类不能装父类,所以父类变成子类,如:object
变成string
感受是不和谐的
协变和逆变用来修饰泛型字母,只有泛型接口和泛型委托能用
“逆变”的“逆”读音为“ni
”,所以可以跟in
相互记忆。
作用
out:只能作为返回值
delegate T TestOut<out T>();
in:只能用在参数
delegate void TestIn<in T>(T value);
综合:
interface ITest<in T, out K>
{
public K Test(T v);
}
协变:父类泛型委托装子类泛型委托
逆变:子类泛型委托装父类泛型委托
多线程
概念
进程(process):操作系统(OS)里启动的程序,每一个程序就是一个进程
线程(thread):运行在进程里,是进程中的实际运作单位,是OS能调度的最小单位
示例
IsBackground
是否为后台线程,
如果为false
主线程结束,需要等待子线程结束才能退出程序;
如果为true
主线程结束,子线程也跟着结束
private static bool _isRunning = true; // 线程运行标识符,控制线程是否运行
public static void Main(string[] args) // 主线程
{
Thread thread = new Thread(Start) // 创建子线程
{
IsBackground = true // 设置为后台线程
};
thread.Start(); // 启动子线程
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
_isRunning = false;
}
private static void Start() // 子线程
{
while (_isRunning) // 线程运行标识符为false,结束此线程运行
{
Console.WriteLine("New thread logic.");
Thread.Sleep(10); // 当前线程休眠10ms
}
}
数据共享
问题
多个线程同时访问/修改同一个变量会出现意想不到的问题
解决问题
通过对要访问的引用变量进行加锁
lock(引用类型对象) {}
示例
private static object _obj = new();
private static int _num = 100;
public static void Main(string[] args) // 主线程
{
while(true)
{
lock(_obj)
{
_num += 100;
}
}
}
private static void Start() // 子线程
{
while (true)
{
lock(_obj)
{
_num -= 100;
}
}
}
反射
元数据
用来描述数据的数据
有关程序以及类型的数据被称为元数据,它们被保存在程序集中
反射概念
程序在运行的时候,可以查看其它或自身程序集的元数据,这个查看的行为叫做反射;
即,通过反射可以在程序运行的时候,对自身或其它程序集中的类、函数、对象等等进行实例化、执行等操作
语法
Type
是反射功能的基础,是访问元数据的主要方式
使用Type
获取有关类声明的信息,成员(构造函数、方法、字段、属性和类的事件)
获取Type
object.GetType()
int a = 33;
Type type = a.GetType();
Console.WriteLine(type); // System.Int32
typeof(类型)
Type type = typeof(int);
Console.WriteLine(type); // System.Int32
Type.GetType("类名")
类名必须包含命名空间
Type type = Type.GetType("System.Int32");
Console.WriteLine(type); // System.Int32
获取类的使用公共成员
MemberInfo[] infos = Type对象.GetMembers();
类(10-Type)
class Test
{
private int _i = 1;
public int J = 2;
public string Str = "reflect";
public Test()
{
}
public Test(int i)
{
_i = i;
}
public Test(int i, string str) : this(i)
{
Str = str;
}
public void Speak()
{
Console.WriteLine("Speak.");
}
}
主函数
using System.Reflection;
public static void Main(string[] args)
{
Type type = typeof(Test);
MemberInfo[] infos = type.GetMembers();
foreach (var info in infos)
{
Console.WriteLine(info);
}
}
输出
Void Speak()
System.Type GetType()
System.String ToString()
Boolean Equals(System.Object)
Int32 GetHashCode()
Void .ctor() // 类的构造函数
Void .ctor(Int32)
Void .ctor(Int32, System.String)
Int32 J
System.String Str
获取类的构造函数并调用
Type type = typeof(Test);
// 获取所有构造函数
ConstructorInfo[] ctorInfos = type.GetConstructors();
// 获取无参构造函数
ConstructorInfo ctorInfo = type.GetConstructor(new Type[0]);
// 获取有参构造函数,按顺序传入构造函数的参数
ConstructorInfo ctorInfo2 = type.GetConstructor(new Type[] { typeof(int), typeof(string) });
// 执行构造函数,按顺序往object数组传入值,没有用null
object obj = ctorInfo.Invoke(null);
object obj2 = ctorInfo2.Invoke(new Object[] { 233, "OKK" });
// 转化为对应类
Test test = obj as Test;
Test test2 = obj2 as Test;
// 调用类对象的函数
test.Speak();
test2.Speak();
Speak 1 ---> reflect
Speak 233 ---> OKK
获取类的公共成员变量
Type type = typeof(Test);
// 获取所有成员变量
FieldInfo[] fieldInfos = type.GetFields();
// 获取指定名称的公共成员变量
FieldInfo field = type.GetField("Str");
Test test = new Test(100, "Jok");
// 获取对象的值
Console.WriteLine(field.GetValue(test));
// 设置对象的值
field.SetValue(test, "OvO");
Console.WriteLine(test.Str);
Jok
OvO
获取类的公共成员方法
获取方法:名称获取,如果有重载,第二个参数需要按顺序传入参数类型
调用方法:传入执行对象和参数数组,如果为静态方法,则第一个参数null
Type strType = typeof(string);
// 获取所有公共成员方法
MethodInfo[] methodInfos = strType.GetMethods();
// 获取公共成员方法(如果有重载,需要按顺序传入参数类型)
MethodInfo subStr = strType.GetMethod("Substring", new[] { typeof(int), typeof(int) });
string str = "Hello fu";
// 调用方法,传入 执行对象 和 参数数组,如果为静态方法,则第一个参数为null
object result = subStr.Invoke(str, new object[] { 1, 3 });
Console.WriteLine(result);
ell
Activator
用于快速实例化Type对象的类
无参构造
// 获取类型
Type type = typeof(Test);
// 快速实例化对象,默认无参构造
object obj = Activator.CreateInstance(type);
// 转换类
Test test = obj as Test;
// 调用类方法
test.Speak();
Speak 1 ---> reflect
有参构造
// 获取类型
Type type = typeof(Test);
// 快速实例化对象,第二个参数为变长参数,直接传入值
object obj = Activator.CreateInstance(type, 250, "olmn");
// 转换类
Test test = obj as Test;
// 调用类方法
test.Speak();
Speak 250 ---> olmn
Assembly
程序集类
主要用来加载其它程序集(不是自己的程序集),比如dll
文件
加载程序集函数
加载同一文件下的其它程序集:
Assembly.Load("程序集名称")
加载不在同一文件下的其它程序集:
Assembly.LoadFrom("包含程序集清单的文件的名称或路径")
Assembly.LoadFile("要加载的文件的完全限定路径")
示例
// 加载指定程序集
Assembly assembly = Assembly.LoadFrom(@"D:\Debug\net7.0\CSharpLearn.dll");
// 获取类对象
Type testClassType = assembly.GetType("CSharpLearn.Test");
// 构造类
object testClass = Activator.CreateInstance(testClassType);
// 通过反射获取方法
MethodInfo speak = testClassType.GetMethod("Speak");
// 调用类方法
speak.Invoke(testClass, null);
Speak 1 ---> reflect
特性
本质是个类,可以利用这种特性类为元数据添加额外信息,之后可以通过反射来获取这些额外信息
语法
class 类名称Attribute : Attribute
{
}
示例
定义特性类
class MyCustomAttribute : Attribute
{
public string Info;
public MyCustomAttribute(string info)
{
Info = info;
}
}
特性名称为MyCustom
使用特性类
[MyCustom("My Class")]
public class ReflectLearn
{
[MyCustom("My Id")]
private int _id = 14;
[MyCustom("My Run")]
public static void Run([MyCustom("My param")]float mul)
{
}
}
为类、成员变量、成员方法等等添加额外信息
反射获取特性
Type type = typeof(ReflectLearn);
// 判断是否使用了某个特性 IsDefined(特性类类型,是否搜索继承链)
Console.WriteLine(type.IsDefined(typeof(MyCustomAttribute), false));
// 获取type类使用的特性类对象
object[] attributes = type.GetCustomAttributes(true);
foreach (var attribute in attributes)
{
if (attribute is MyCustomAttribute)
{
// 获取特性的成员对象
Console.WriteLine((attribute as MyCustomAttribute).Info);
// 调用特性的方法
(attribute as MyCustomAttribute).Print();
}
}
True
My Class
Hello attribute.
限定自定义特性的使用范围
可以在自定义特性类上面使用AttributeUsage
修饰使用范围
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
class MyCustomAttribute : Attribute
{
public string Info;
public MyCustomAttribute(string info)
{
Info = info;
}
public void Print()
{
Console.WriteLine("Hello attribute.");
}
}
AttributeTargets
:特性能用在哪里
AllowMultiple
:是否允许多个特性实例用在同一个目标上
Inherited
:特性能否被派生类和重写成员继承
过时特性
[Obsolete(过时提示内容, 是否报错)]
[Obsolete("This method was obsolete.", false)]
public static void Run(float mul)
{
}
-----------------------
ReflectLearn.Run(.34f);
条件编译特性
一般和#define
配合使用
using System.Diagnostics;
[Conditional("标识符")]
#define USING
using System.Diagnostics;
[Conditional("USING")] // 有了USING标识符才会编译此函数
static void Func()
{
Console.WriteLine("Func");
}
static void Main(string[] args)
{
Func(); // 如果没有USING标识符,就不会调用
}
外部DLL函数特性
用来定义该函数是由非.Net(C#)定义的函数,一般用来调用C或C++的DLL中的方法
using System.Runtime.InteropServices;
[DllImport("dll")]
public static extern 返回值类型 dll中的函数名(dll中的参数列表);
[DllImport("Test.dll")]
public static extern void Func();
static void Main(string[] args)
{
Func();
}
迭代器
概念
iterator
又称光标(cursor
),是一种程序设计的软件设计模式,迭代器模式提供了一个方法顺序访问一个聚合对象中的各个元素
是容器对象用来遍历的接口,使用foreach
之前必须先实现迭代器
标准迭代器的实现方法
IEnumerable
:是否可以迭代接口
IEnumerator
:迭代器接口
foreach
执行顺序:
自动调用
GetEnumerator()
光标移动到下一个位置
MoveNext()
如果为
true
,返回Current
;否则跳出循环
类
class CustomList : IEnumerable, IEnumerator
{
public int[] List { get; set; }
public CustomList()
{
List = new[] { 1, 2, 3, 4, 5, 6 };
}
// 声明光标
private int cursor = -1;
// 当前光标所在位置的值
public object Current => List[cursor];
// 1.foreach会自动调用这个方法
public IEnumerator GetEnumerator()
{
// 重置光标位置,用以下次循环使用
Reset();
return this;
}
// 2.光标移动到下一个位置
// 3.如果为true,返回Current;否则跳出循环
public bool MoveNext()
{
// 移动光标
++cursor;
// 如果光标表示的索引超出List的长度,则跳出循环
return cursor < List.Length;
}
// 重置光标位置至初始位置
public void Reset()
{
cursor = -1;
}
}
主函数
public static void Main(string args)
{
CustomList customList = new CustomList();
foreach (int cl in customList)
{
Console.WriteLine(cl);
}
foreach (int cl in customList)
{
Console.WriteLine(cl);
}
}
输出
1
2
3
4
5
6
1
2
3
4
5
6
语法糖实现迭代器
想让自定义类 通过foreach
遍历只要实现IEnumerable
接口中的GetEnumerator()
方法即可
使用yield return
返回就可以
class CustomList : IEnumerable
{
public int[] List { get; set; }
public CustomList()
{
List = new[] { 1, 2, 3, 4, 5, 6 };
}
// 只要实现此方法即可
public IEnumerator GetEnumerator()
{
for (int i = 0; i < List.Length; i++)
{
// C#语法糖,系统自动生成IEnumerator的函数
yield return List[i];
}
}
}
语法糖实现泛型类迭代器
class CustomList<T> : IEnumerable
{
private T[] _array;
public IEnumerator GetEnumerator()
{
foreach (var t in _array)
{
yield return t;
}
}
}
特殊语法
var隐式类型
表示任意类型
不能做为类成员,只能用于临时变量,一般写在函数中;
必须初始化;
var a = 99;
大括号设置对象初始值
类名 变量名 = new 类名(参数值){ 成员变量1 = 值1, 成员变量2 = 值2, ... };
Person p = new Person(200, "ok"){ age = 11, idCard = 10235345, name = "lok" };
匿名类型
var 变量名 = new { 变量1 = 值1, 变量2 = 值2, ... }
var v = new { age = 23, ok = "kok", cou = .34f };
可空类型(?)
声明时,在值类型后写上?就可以为null
变量类型? 变量名 = 值;
int? val = null;
判断是否为空
变量名.HasValue
安全获取可空类型的值
为空默认返回默认值
int? value = null;
value.GetValueOrDefault();
指定为空时返回的默认值
int? value = null;
value.GetValueOrDefault(38);
自动判断引用类型是否为空
如果为空则不执行
变量名?.方法名(值)
变量名?[值]
object obj = null;
obj?.ToString(); // 相当于:if (obj != null) { obj.ToString(); }
int[] array = null;
arr?[0];
空合并操作符(??)
如果左边为null
,就返回右边,否则左边
可以为null
的类型都可以使用
左值 ?? 右值
相当于:
if (左值 == null)
{
return 右值;
}
else
{
return 左值;
}
int? v = null;
Console.WriteLine(v ?? 0); // 0