阅前提示
本篇文章主旨在于 总结在使用C#过程中一些模糊的概念并深入理解其本质。
并记录使用C#语言的一些技巧,提高我们的代码质量。
为了浏览舒适,涉及内容或示例代码较多的部分会提供跳转来阅读。
适合人群:对C# 有一定使用基础的人
阅读方式:浏览,是否已知并加深认识
C#Skills
1.可空修饰符 ?
// int i = null; 因为值类型不可以被赋值成Null这里会报错
int? i = null; // 所以要使用 ? 修饰符
Nullable
struct Nullable<T>
{
bool hasValue;
T value;
}
2.Box/UnBox
装箱:值类型转引用类型叫,过程:先在堆上分配新存储位置,将值类型数据复制到新存储位置中,将结果变为对新存储位置的引用。
反之,拆箱 ,过程:判断装箱的值类型是否可被转换成拆卸类型,赋值堆中的存储值。
int number = 10;
object obj;
obj = number;//boxing IL_000... box
number = (int)obj;//Unboxing IL_000... unbox.any
装箱,拆箱会带来性能上的损耗,所以要避免不必要的装箱拆箱。
避免可变的值类型
这会让你在装箱拆箱上分不清方向,所以处于规范,避免可变的值类型。
3.终结器
存在不确定性,不知道何时被回收器调用
~ClassName(){} 被垃圾回收器主动调用
IDisposable
结合using实现确定性终结
public void Dispose()
{
...Dispose
System.GC.SuppressFinalize(this);
}
SuppressFinalize:的作用是将该实例从(f-reachable)队列中主动移除。
f-reachable终结队列:这里存放着即将被执行终结方法的实例。只有终结方法被调用后,才能对这个对象进行垃圾回收,不然将一直存在引用。这个时间是不确定的,所以如果可以提前对此进行回收的话,就主动将其移除此队列。
4.System.Lazy< T >
推迟初始化
Lazy<classA> a = new Lazy<classA>(()=> new classA());
var b = a.value;//当第一次访问value时执行委托
5.协变与逆变
协变
观点:我们无法将List< string > 转换成List< object >,因为两者不具有协变性。
原因:因为其都具备写入的功能,无法保证数据安全性。
安全协变性
原理:防止对 T 的输入
使用 out 修饰符
interface IReadOnlyA<out T>
{
T First{get;}
}
协变的限制
1.只有泛型接口和泛型委托才可以协变;
2.“来源”和“目标”必须是引用类型。
3.接口和委托必须声明为支持协变,编译器必须验证协变类型只用于输出
逆变
与协变相反,限制条件类似。使用 in 修饰符,防止对 T 的输出
interface ICompareA<in T>
{
bool Compare(T a,T b);
}
数组的协变
Fruits[10] f = new Apple[10];//这是被允许的
但对于可读可写的类型,这并不是安全的协变。
IEnumerable<Fruits> f = new Apple[10];//使用只读类型来确保安全
避免不安全的数组协变,考虑使用只读接口来安全转换
6.Func&Action
System.Func
代表有返回值的方法,最后一个类型参数总是委托的返回类型。
public delegate TResult Func<in T1,in T2,out TResult>(T1 a,T2 b)
System.Action
代表返回 void 的方法
public delegate void Action<in T>(T a)
7.Lambda表达式树
persons.Where(person => person.Name == "AAAA");
对于这个Lambda表达式,在不同情况下具有两种含义。
IEnumerable
代表 委托参数,比如 Func< T,bool >
public IEnumerable<T> Where<T>(this Ienumerable<T> collection,Func<T,bool> predicate)
IQueryable
代表 表达式树参数,比如 Expression<Func<T,bool>>
public IQueryable<T> Where<T>(this ...,Expression<Func<T,bool>> predicate)
区别
委托:生成编译好的用于实现匿名函数的代码,这是实实在在的逻辑。
表达式树:生成的是对Lambda表达式进行描述的数据,这是再执行时用来分析Lambda,用分析得到的数据构造查询,并针对数据库执行。
简而言之:表达式树不是可执行的代码,而是数据,而委托是可执行的代码。
8.MulticastDelegate
多播委托
创建委托时,编译器自动使用System.MulticastDelegate类型。
添加方法时,MD类会创建委托类型的一个新实例,在新实例中为新增的方法存储对象引用和方法引用,并在委托实例列表中添加新委托实例。实际上,它维护着一个Delegate对象链表。
9.Event 安全地封装委托
相比于delegate的随意性,引入了新的关键字 event
表现
public event EventHandler<TArgs> TempEvent = delegate{};
优点
1.被修饰的委托无法被赋值操作
2.只能在包容类内部进行调用,外部只可以使用+=,-=
EventHandler
public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e) where TEventArgs:EventArgs;
这是一个为了编码规范而生的新的委托类型。包含两个参数,sender 包含对调用委托对象的引用,args 派生于EventArgs 用来包含事件的附加数据。
10.Foreach
本块内容涉及代码较多,单独提供跳转进行阅读。
C# IEnumerable Foreach 深入理解
11.Linq查询
Linq.IEnumerable 拓展了IEnumerable的功能
延迟执行
LINQ中大部分查询运算符都有一个非常重要的特性:延迟执行。这意味着,他们不是在查询创建的时候执行,而是在遍历的时候执行(换句话说,当enumerator的MoveNext方法被调用时)
List<int> a = new List<int>();
a.Add(3);
var q = a.Where(x=>(x%2) == 0);
q = q.Select(x=>x*10);
a.Add(2);a.Add(4);
UnityEngine.Debug.Log("---foreach a----");
foreach(int v in a)
{Log(v);}// 3 2 4
Log("---foreach q----");
foreach(int v in q)
{Log(v);} // 20 40
Log("---foreach q again----");
foreach(int v in q)
{Log(v);} // 20 40
特别注意:如果想安全的操作集合,那再查询之后使用 ToXXX转换成想要的集合类型
12.BinarySearch
这种搜索采用的是二分法搜索算法,前提是要求元素已经排好序。若没有返回值,它返回一个负数,该值的按位取反(~)结果是“大于被查找元素的下一个元素”的索引,若没有则是元素的总数。方便插入新值。
List<.string> list = new List<string>(){"p","a","c"};
list.Sort();
var search = list.BinarySearch("e");
if(search < 0)
list.Insert(~search,"e");
13.特性
利用特性,可以指定与被修饰的构造有关的额外元数据。特性是将额外数据关联到属性(以及其他构造)的一种方式。[]
public class SimpleAttribute:Attribute{}
[Simple]
somethings...
AttributeUsageAttribute
1.ValidOn 限制特性能够修饰的东西 AttributeTargets
2.AllowMultiple 这个属性标记了我们的定制特性能否被重复放置在同一个程序实体前多次
3.Inherited 表明当特性被放置在一个基类上时,它能否被派生类所继承
[AttributeUsage(AttributeTargets.Class,AllowMultiple = true)]
public class SimpleAttribute:Attribute{}
//表现
[Simple]
[Simple]
public class A{}
系统定义的特性
[Flags]
[Flags]
public enum FileAttributes
{
ReadOnly = 1<<0;
Hidden = 1<<1;
}
//用来改变ToString() 和 Parse()的行为
// 输出会变成 ReadOnly、Hidden 而不是数字
[("...")]
#define C_A
[Conditional("C_A")]
public void A(){}
//这个只有在 #define 存在时才能正常执行
//类似于 #if/#endif
[Obsolete]
[Obsolet("警告内容"),true]
public void A(){}
// 编译时会提示警告
//可填两个参数,内容 和 是否强制警告视为错误
[Serializable] [NonSerialized]
可序列化/不可序列化
[Serializable]
public class Skill13Data
{
public string content;
[NonSerialized]
public string name;
public Skill13Data(string content){
name = "Skill13Dara";
this.content = content;
}
}
using(Stream stream = File.Open("Test.bin",FileMode.Create))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream,new Skill13Data("Hello world"));
}
using(Stream stream = File.Open("Test.bin",FileMode.Open))
{
BinaryFormatter formatter = new BinaryFormatter();
var data = (Skill13Data)formatter.Deserialize(stream);
Log(data.content); // Hello world
Log(data.name); // Null 因为被标记了不可序列化
}
14.线程
基础
简单的线程实现
ThreadStart ts = ThreadMethod;//该线程要执行的方法转换成委托型ThreadStart
Thread subT = new Thread(ts);//创建线程
subT.Start();//开始该线程
subT.Join();//让主线程等待该线程结束
线程池
// 设置最大线程数
ThreadPool.SetMaxThreads(5,5);
//将任务添加进线程池,若线程已满则会排队等待
ThreadPool.QueueUserWorkItem(ThreadPoolMethod);
public void ThreadPoolMethod(object state){...}
.NET 4.0
.NET 4.0 后的TPL(Task Parallel Library) 任务进行库 提供了更加简便的异步任务编程模式。将异步工作抽象到Task对象中,通过提供代表工作单元的对象,TPL使我们能通过编程将小任务合并成大任务,从而建立起一个工作流。
任务是对象,其中封装了以异步方式执行的工作
不同于委托,委托是同步的,执行委托需要等待委托的结束。
任务是异步的,启动任务,会立即返回调用者,任务在另一个线程上异步执行。
using System.Threading.Tasks;
// 两种创建方式
Task t1 = new Task(()=>{...});//Action
t1.Start();
Task<string> t2 = Task.Run(()=>{...return"Hellow";});//Func
//链接任务
Task task = Task
.Run(()=>{Console.WriteLine("Hellow");})
.ContinueWith((t)=>{Console.WriteLine("Wrold");});
//取消任务
CancellationTokenSource tokenSource = new CancellationTokenSource();
Task task = Task.Run(()=>{},tokenSource.Token);
tokenSource.Cancel();// 实际上是设置tokenSource.Token.IsCancellationRequested该字段等待下一次检查,ture 则while循环退出。
N.关键字
sealed
修饰类避免被继承,修饰方法避免被重写
using
三种用法
using System; | 引用命名空间 |
---|---|
using Timer = System.Timers.Timer | 为空间或类型取别名 |
using (DisposableSomething){...} | 作为语句用于定义一定范围,在最后释放对象 |
第三点中,使用的对象必须实现IDisposable接口,保证该对象可被释放。可有多个参数但必须是同类型
using(Something){....} 相当于
try{...}
finally{ ((IDisposable)Something).Dispose()}
param
方法参数数组
public void F(param string[] st){}
F("Hello","World");
注意:参数必须是数组;不能与ref out 组合使用;
new
new AClass(); | 运算符:创建对象调用构造函数 |
---|---|
new public int a; | 修饰符:显式隐藏从基类继承的成员 |
class AA<T> where T:new() | 约束:泛型声明中约束可能用作类型参数的参数的类型 |
This
代表当前实例 | this.a |
---|---|
串联构造函数 | public A(string a):this(...) |
拓展方法的第一个参数修饰符 | public void F(this int a){...} |
@
1.字符串开头声明,不需要对转义字符加(\)
2.@开头的字符串可以跨行,方便阅读
3.在关键字前面使用可以修饰成变量名
string path = @"c:\docs\a.txt" ;// c:\\docs\\a.txt
string t = @"hellow ""V"" " ;// "hellow \"V\" "
string tt = @" aaa
bbb
ccc
";
string @string = "string";
extern
修饰符用于声明在外部实现的方法。
托管代码时与 DllImport 属性一起使用;在这种情况下,该方法还必须声明为 static,如下面的示例所示:
[DllImport("avifil32.dll")]
private static extern void AVIFileInit();
partial
Partial是局部类型的标志。局部类型可以实现将一个类、结构或接口分成几个部分,分别放在在几个不同的.cs文件中(当然也可以放在同一个.cs文件中)。在程序进行编译之后,将会合并成一个完整的类。因此局部类型并没有看起来那么难以理解,使用partial只是让类变得更容易管理,实际使用时和普通的类一样。
如果一个类内容太多,或者存在一些自动生成的部分,可以使用该关键字,将类拆分成几个文件来管理。
public partial class AClass{...}
yield
yield关键字是一种语法糖,实际上还是通过实现IEnumberable、IEnumberable< T >、IEnumberator和IEnumberator< T >接口来满足迭代功能
详细解释请看上方 10.Foreach