写在前面
发现一个买实体书的好处吧,当你发现屋子里的乱七八糟的东西越来越多,想清理掉的时候,发现一堆没怎么翻过的书在那里,就只能把它看完,做好笔记再扔了。
这本书真的是比砖头还要厚,里面的各种知识点都很详细。但因为一些东西太过基础,一些东西又不是我想做的方向,所以我就对着目录圈出了自己觉得需要看的地方。下面做一个笔记,算是填充自己的资料库吧。
&& 发现里面有些东西有点旧了,但还是现看完再说,新添的特性再额外补充一下。
语法方面
- 关于两个main()函数:一般C#程序中,只允许有一个静态main函数作为入口,否则会编译错误。(看目录的时候还以为多个main函数可以有什么特殊操作特殊用法,结果并没有)
- 闭包:声明匿名函数的时候,如果引用了匿名函数内部引用了外部的量,就会形成一个闭包。关于闭包,我个人理解就是匿名函数在实际执行的时候,会隐式创建一个类,这个类包含构造方法和一个与匿名函数相同的public方法,外部变量通过构造方法传到隐式类里面来,调用匿名函数时执行隐式类的public方法。了解了大致的原理,我们就可以知道,首先写程序的时候应该避免频繁使用带闭包的匿名函数;其次注意匿名函数中[[引用]]的外部变量,每次调用匿名函数,外部变量的值都是最新值,比如for循环中Dotween的OnComplete中输出循环变量 i 值,输出的 i 值不是从0开始,而是循环最大值(这个高版本语言已经升级改掉了,不用在意这条,但低版本语言上还是有这个特性的,仍然需要注意)
- 逆变和协变:协变out,逆变in。用在接口上,可以控制传参和返回值的类型大小。这地方确实比较绕。下面有个简单例子,帮助理解。其中A大于B表示A包含B,能用B的地方一定能用A。(一定要看注释)(个人觉得这个东西自然而然的就用出来了,死扣定义没什么大用,用多了就好了。&&写错了属于语法错误,有报错的,也不用怕出稀奇古怪的bug)
/// 1. string 是 object 的子类 ==》 string < object
/// 2. 作为输入值,能输入object的地方一定能输入string
/// ==》 action<in string> 包含 action<in object>
/// ==》 action<in string> 大于 action<in object>
/// ==》 大小关系发生变化,为逆变
/// 3. 作为输出值,能输出object的地方一定能输出string
/// ==> func<out object> 包含 func<out string>
/// ==> func<out string> 小于 func<out object>
/// ==> 大小关系不变,为协变
/// 4. 上面的2,3只是简单的举例,实际使用时,可以自己控制in,out参数
Action<object> v = null;
Action<string> c = v;
Func<string> s = null;
Func<object> d = ()=>
{
return s();
};
-
dynamic
类型,可以在运行时确定变量的类型,而不是在编译阶段。但有两个限制,一个是动态类型不支持扩展方法,另一个是动态类型不能作为匿名函数的参数。个人觉得,dynamic
在反射之类的东西里面会比较有用,平时一般开发中,基本不会用上这个东西。&&C#本身并不是动态语言,为了实现这个动态语言的功能,C#内部做了很多很多的操作,这也就导致了一个问题,他的效率并不高。
字符串
string类
需要注意的是,string类方法的一系列操作,基本都是重新创建新字符串,进行一系列处理后再将旧字符串丢弃,新字符串返回。所以,一些长字符串操作不要用string,影响效率。另外,除了常用的字符串方法,比如format
之类的,还有其他的字符串方法,可以简单实现我们的需求。遇到类似的问题要先记得查一下API,尽量不要直接自己上手写,一是非学习情况下造轮子没什么意义,二是自己造的轮子还不一定好用。长字符串的处理:使用
StringBuilder
StringBuilder
仅能够替换,追加,删除字符串中的文本,而且它的效率很高。
StringBuilder
在能够设置Capacity
,也就是操作字符串的空间大小。所有对字符串的操作都在这个空间中进行,而不用额外去申请内存空间,从而提高操作字符串的效率。在默认不设置Capacity
的情况下,StringBuilder
创建的空间会比初始化所用的字符串大,&&如果操作的字符超出所设置的Capacity
,一般会自动翻倍。
值得额外注意⚠️的是,只有对字符串频繁进行操作,才能获得StringBuilder
的性能优势。关于
Fromat
需要额外知道的就是通过实现IFormattable
接口,可以自定义format格式。下面是一个简单的例子:
public class A : IFormattable
{
public string content;
public string ToString(string format, IFormatProvider formatProvider)
{
switch (format)
{
case "UP":
return content.ToUpper();
break;
case "LOW":
return content.ToLower();
break;
default:
return content;
break;
}
}
}
var a = new A();
a.content = "Hello World !!!";
print($"{a:LOW}");
print(a.ToString("UP", null));
- 正则表达式
关键字Regex
内存管理和指针
- 堆和栈
- 栈:存储值类型数据
- 托管堆:存储引用类型数据
- C#中的指针
- 在C#中使用指针需要添加
unsafe
标记 - 在类型后面添加
*
表示对应类型的指针。eg.int*
byte*[]
-
&
取地址操作;*
获取地址内容操作
- 在C#中使用指针需要添加
-
sizeof
确定各种数据类型所占的空间大小(字节数) -
stackalloc
分配一定大小的内存。下面是一个高速数组的例子:(需要注意的是,这种数据是没有越界报错的,需要自己额外检查)
unsafe static void Test()
{
int* intArray = stackalloc int[10];
for (int i = 0; i < 10; i++)
{
intArray[i] = i;
}
for (int i = 0; i < 10; i++)
{
Console.WriteLine(*(intArray + i));
}
}
反射
- 这个东西游戏开发代码中一般是用不到的。其他的.net具体是什么样子就不清楚了,没有做过.net的正式商业项目,只是自己写过demo工具之类。欢迎大佬告知。
- 其实就是用名字来找对应的类型方法属性等等,然后对其进行动态创建调用方法等操作。
- 个人的使用感受:根据名字=》找程序集=》找类型=》找方法属性字段等等。找到你想找的量进行操作就好了。
但这个名字有时候容易写不对。就Unity来说,Unity自己的非编辑器C#脚本是打在一个dll里面的,写编辑器工具时,不好直接填那个dll的名字。我当时是找了一个非编辑器类,用这个类取它所在的程序集,然后用获取到的这个程序集进行反射,来找其他的想要的类。算是一个偷懒的办法吧。
异步
- 最最基本的,使用
Thread
实现。这个没什么太需要注意的。需要注意一下的就是启动新线程消耗会比较大,要避免频繁创建线程。&&C#提供了其他的异步类和接口,这个可以基本不用了。 -
Task
,下面是几种创建&&启动方式。类&接口知道了,其他的细节需要的时候直接查一下就好了,这里不再记录。
创建
Task.Factory.StartNew
Task.Run
Task task1 = new Task(() =>
{
Console.WriteLine(123);
});
task1.Start();
阻塞等待,当然还有其他的接口,用的时候直接看补全提示就好了
Task.WaitAll(task1)
-
CancellationTokenSource
取消常驻线程辅助类。
它的主要作用就是标记量,但相比普通的bool
和int
,使用方便&&有自带的触发回掉接口。 -
async
await
,用法感觉上和协程的yield return
差不多。
class Program
{
static void Main(string[] args)
{
var content = Test1().Result;//这个属性是阻塞的
Console.WriteLine(content);
}
static async Task<string> Test1()//这里返回值还可以是`Task` `void`
{
string path = "";
FileStream fs = new FileStream(path, FileMode.Open);
StreamReader sr = new StreamReader(fs);
string content = await sr.ReadToEndAsync();//这里等待读取完成
return content;
}
}
- 并行
Parallel
:注意并行不保证执行顺序
Parallel
有三个静态方法,分别是Invoke
,For
,Foreach
。-
Parallel.Invoke(params Action[] actions)
:能够同时执行参数中的方法。 -
Parallel.For
:多次执行一个方法。如果需要执行的次数大于当前任务线程的数量,则同时执行的方法数量为当前最大任务线程数,这一批次执行完毕再执行下一个批。ParallelLoopState
对象的Break
方法能够提前终止该次并行。
3.Parallel.For(0, 40, (i, p) => { string content = $"i:{i} taskId:{Task.CurrentId} ThreadId: {Thread.CurrentThread.ManagedThreadId}"; Console.WriteLine(content); Thread.Sleep(1000); });
Parallel.Foreach
:并行执行一个方法。第二个Action
参数中,第一个为迭代的值,第二个是并行状态变量,第三个为迭代次数。string[] test = new[] { "Hello", "World", "HHH", "123", "456", }; Parallel.ForEach(test, (a, p, i) => { Console.WriteLine($"content:{a}, long:{i}"); });
-
-
System.Timer.Timer
计时器,延迟多长时间触发事件。 - 关于线程间数据同步,第一个原则就是能不同步就不同步。必须同步的,除了简单的使用
lock
,还有其他的Spinlock
,Interlocked
等(++i不是线程安全的)。
一个实用工具
使用ildasm工具进行代码分析(反编译)。
断断续续,看一点写一点拖了好久,终于吧想看的看完了。
最后感谢大神写的好文章
深入理解 C# 协变和逆变,给了我很大启发。
C# Task和async/await详解,介绍的很详细。