目录:缓存
1,文件操做安全
2,Debug、Trace类多线程
3,条件编译运维
4,MethodImpl 特性函数
5,CLSComplianAttribute工具
6,必要时自定义类型别名性能
最近在阅读 .NET Core Runtime 的源码,参考大佬的代码,学习编写技巧和提升代码水平。学习过程当中将学习心得和值得应用到项目中的代码片断记录下来,供往后查阅。学习
这段代码在 System.Private.CoreLib
下,对 System.IO.File 中的代码进行精简,供 CLR 使用。优化
当使用文件时,要提早判断文件路径是否存在,平常项目中要使用到文件的地方应该很多,能够统一一个判断文件是否存在的方法:this
public static bool Exists(string? path) { try { // 能够将 string? 改为 string if (path == null) return false; if (path.Length == 0) return false; path = Path.GetFullPath(path); // After normalizing, check whether path ends in directory separator. // Otherwise, FillAttributeInfo removes it and we may return a false positive. // GetFullPath should never return null Debug.Assert(path != null, "File.Exists: GetFullPath returned null"); if (path.Length > 0 && PathInternal.IsDirectorySeparator(path[^1])) { return false; } return InternalExists(path); } catch (ArgumentException) { } catch (NotSupportedException) { } // Security can throw this on ":" catch (SecurityException) { } catch (IOException) { } catch (UnauthorizedAccessException) { } return false; }
建议项目中对路径进行最终处理的时候,都转换为绝对路径:
Path.GetFullPath(path)
固然,相对路径会被 .NET 正确识别,可是对于运维排查问题和各方面考虑,绝对路径容易定位具体位置和排错。
在编写代码时,使用相对路径,不要写死,提升灵活性;在运行阶段将其转为绝对路径;
上面的 NotSupportedException
等异常是操做文件中可能出现的各类异常状况,对于跨平台应用来讲,这些异常可能都是很常见的,提早将其异常类型识别处理,能够优化文件处理逻辑以及便于筛查处理错误。
这段代码在 System.Private.CoreLib 中。
有个读取文件转换为 byte[] 的方法以下:
public static byte[] ReadAllBytes(string path) { // bufferSize == 1 used to avoid unnecessary buffer in FileStream using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) { long fileLength = fs.Length; if (fileLength > int.MaxValue) throw new IOException(SR.IO_FileTooLong2GB); int index = 0; int count = (int)fileLength; byte[] bytes = new byte[count]; while (count > 0) { int n = fs.Read(bytes, index, count); if (n == 0) throw Error.GetEndOfFile(); index += n; count -= n; } return bytes; } }
能够看到 FileStream 的使用,若是单纯是读取文件内容,能够参考里面的代码:
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)
上面的代码一样也存在 File.ReadAllBytes
与之对应, File.ReadAllBytes 内部是使用 InternalReadAllBytes
来处理文档读取:
private static byte[] InternalReadAllBytes(String path, bool checkHost) { byte[] bytes; // 此 FileStream 的构造函数不是 public ,开发者不能使用 using(FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, FileStream.DefaultBufferSize, FileOptions.None, Path.GetFileName(path), false, false, checkHost)) { // Do a blocking read int index = 0; long fileLength = fs.Length; if (fileLength > Int32.MaxValue) throw new IOException(Environment.GetResourceString("IO.IO_FileTooLong2GB")); int count = (int) fileLength; bytes = new byte[count]; while(count > 0) { int n = fs.Read(bytes, index, count); if (n == 0) __Error.EndOfFile(); index += n; count -= n; } } return bytes; }
这段说明咱们能够放心使用 File
静态类中的函数,由于里面已经处理好一些逻辑了,而且自动释放文件。
若是咱们手动 new FileStream
,则要判断一些状况,以避免使用时报错,最好参考一下上面的代码。
.NET 文件流缓存大小默认是 4096
字节:
internal const int DefaultBufferSize = 4096;
这段代码在 File 类中定义,开发者不能设置缓存块的大小,大多数状况下,4k 是最优的块大小。
ReadAllBytes 的文件大小上限是 2 GB。
这两个类的命名空间为 System.Diagnostics
,Debug 、Trace 提供一组有助于调试代码的方法和属性。
Debug 中的全部函数都不会在 Release 中有效,而且全部输出流不会在控制台显示,必须注册侦听器才能读取这些流。
Debug 能够打印调试信息并使用断言检查逻辑,使代码更可靠,而不会影响发运产品的性能和代码大小。
这类输出方法有 Write 、WriteLine 、 WriteIf 和 WriteLineIf 等,这里输出不会直接打印到控制台。
如需将调试信息打印到控制台,能够注册侦听器:
ConsoleTraceListener console = new ConsoleTraceListener(); Trace.Listeners.Add(console);
注意, .NET Core 2.x 以上 Debug 没有 Listeners ,由于 Debug 使用的是 Trace 的侦听器。
咱们能够给 Trace.Listeners 注册侦听器,这样相对于 Debug
等效设置侦听器。
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); Debug.WriteLine("aa");
.NET Core 中的监听器都继承了 TraceListener,如 TextWriterTraceListener、ConsoleTraceListener、DefaultTraceListener。
若是须要输出到文件中,能够自行继承 TextWriterTraceListener
,编写文件流输出,也可使用 DelimitedListTraceListener。
示例:
TraceListener listener = new DelimitedListTraceListener(@"C:\debugfile.txt"); // Add listener. Debug.Listeners.Add(listener); // Write and flush. Debug.WriteLine("Welcome");
处理上述方法输出控制台,也可使用
ConsoleTraceListener console=... ...Listeners.Add(console); // 等效于 var console = new TextWriterTraceListener(Console.Out)
为了格式化输出流,可使用 一下属性控制排版:
属性 | 说明 |
---|---|
AutoFlush | 获取或设置一个值,经过该值指示每次写入后是否应在 Flush() 上调用 Listeners。 |
IndentLevel | 获取或设置缩进级别。 |
IndentSize | 获取或设置缩进的空格数。 |
// 1. Debug.WriteLine("One"); // Indent and then unindent after writing. Debug.Indent(); Debug.WriteLine("Two"); Debug.WriteLine("Three"); Debug.Unindent(); // End. Debug.WriteLine("Four"); // Sleep. System.Threading.Thread.Sleep(10000);
One Two Three Four
.Assert()
方法对咱们调试程序颇有帮助,Assert 向开发人员发送一个强消息。在 IDE 中,断言会中断程序的正常操做,但不会终止应用程序。
.Assert()
的最直观效果是输出程序的断言位置。
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); int value = -1; // A. // If value is ever -1, then a dialog will be shown. Debug.Assert(value != -1, "Value must never be -1."); // B. // If you want to only write a line, use WriteLineIf. Debug.WriteLineIf(value == -1, "Value is -1.");
---- DEBUG ASSERTION FAILED ---- ---- Assert Short Message ---- Value must never be -1. ---- Assert Long Message ---- at Program.Main(String[] args) in ...Program.cs:line 12 Value is -1.
Debug.Prinf()
也能够输出信息,它跟 C 语言的 printf 函数行为一致,将后跟行结束符的消息写入,默认行终止符为回车符后跟一个换行符。
在 IDE 中运行程序时,使用 Debug.Assert()
、Trace.Assert()
等方法 ,条件为 false 时,IDE 会断言,这至关于条件断点。
在非 IDE 环境下,程序会输出一些信息,但不会有中断效果。
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); Trace.Assert(false);
Process terminated. Assertion Failed at Program.Main(String[] args) in C:\ConsoleApp4\Program.cs:line 44
我的认为,能够将 Debug、Trace 引入项目中,与日志组件配合使用。Debug、Trace 用于记录程序运行的诊断信息,便于往后排查程序问题;日志用于记录业务过程,数据信息等。
.Assert()
的原理, 在 true 时什么都不作;在 false 时调用 Fail 函数;若是你不注册侦听器的话,默认也没事可作。
.Assert()
惟一可作的事情是等条件为 false 时,执行 Fail 方法,固然咱们也能够手动直接调用 Fail 方法,Fail 的代码以下:
public static void Fail(string message) { if (UseGlobalLock) { lock (critSec) { foreach (TraceListener listener in Listeners) { listener.Fail(message); if (AutoFlush) listener.Flush(); } } } else { foreach (TraceListener listener in Listeners) { if (!listener.IsThreadSafe) { lock (listener) { listener.Fail(message); if (AutoFlush) listener.Flush(); } } else { listener.Fail(message); if (AutoFlush) listener.Flush(); } } } }
#if
条件编译会隐藏非条件(#else if)代码,咱们开发中极可能会忽略掉这部分代码,当咱们切换条件常量到这部分代码时,极可能由于各类缘由致使报错。
若是使用特性进行条件编译标记,在开发过程当中就能够留意到这部分代码。
[Conditional("DEBUG")]
例如,当使用修改全部引用-修改一个类成员变量或者静态变量名称时,#if
非条件中的代码不会被修改,由于这部分代码“无效”,并且使用 [Conditional("DEBUG")]
的代码则跟条件无关,会被同步修改。
Conditional
特性标记的方法等,在开发过程当中保持有效,当在编译时可能被排除。
代码片断只能使用 #if
了,若是是单个方法,则可使用 Conditional
。
此特性在 System.Runtime.CompilerServices 命名空间中,指定如何实现方法的详细信息。
内联函数使用方法可参考 https://www.whuanle.cn/archives/995
MethodImpl 特性能够影响 JIT 编译器的行为。
没法使用 MemberInfo.GetCustomAttributes
来获取此特性的信息,即不能经过获取特性的方法获取跟 MethodImpl
有关的信息(反射),只能调用 MethodInfo.GetMethodImplementationFlags()
或 ConstructorInfo.GetMethodImplementationFlags ()
来检索。
MethodImpl 能够在方法以及构造函数上使用。
MethodImplOptions 用于设置编译行为,枚举值可组合使用,其枚举说明以下:
枚举 | 枚举值 | 说明 |
---|---|---|
AggressiveInlining | 256 | 如可能应将该方法进行内联。 |
AggressiveOptimization | 512 | 此方法包含一个热路径,且应进行优化。 |
ForwardRef | 16 | 已声明该方法,但在其余位置提供实现。 |
InternalCall | 4096 | 该调用为内部调用,也就是说它调用了在公共语言运行时中实现的方法。 |
NoInlining | 8 | 该方法不能为内联方法。 内联是一种优化方式,经过该方式将方法调用替换为方法体。 |
NoOptimization | 64 | 调试可能的代码生成问题时,该方法不禁实时 (JIT) 编译器或本机代码生成优化(请参阅 Ngen.exe)。 |
PreserveSig | 128 | 彻底按照声明导出方法签名。 |
Synchronized | 32 | 该方法一次性只能在一个线程上执行。 静态方法在类型上锁定,而实例方法在实例上锁定。 只有一个线程可在任意实例函数中执行,且只有一个线程可在任意类的静态函数中执行。 |
Unmanaged | 4 | 此方法在非托管的代码中实现。 |
Synchronized
修饰的方法能够避免多线程中的一些问题,可是不建议对公共类型使用锁定实例或类型上的锁定,由于 Synchronized
能够对非本身的代码的公共类型和实例进行锁定。 这可能会致使死锁或其余同步问题。
意思是说,若是共享的成员已经设置了锁,那么不该该再在 Synchronized
方法中使用,这样双重锁定容易致使死锁以及其余问题。
指示程序元素是否符合公共语言规范 (CLS)。
CLS规范可参考:
https://docs.microsoft.com/en-us/dotnet/standard/language-independence
https://www.ecma-international.org/publications/standards/Ecma-335.htm
全局开启方法:
程序目录下添加一个 AssemblyAttribytes.cs 文件,或者打开 obj 目录,找到 AssemblyAttributes.cs 结尾的文件,如 .NETCoreApp,Version=v3.1.AssemblyAttributes.cs,添加:
using System; // 这行已经有的话不要加 [assembly: CLSCompliant(true)]
以后就能够在代码中使用 [CLSCompliant(true)]
特性。
局部开启:
也能够放在类等成员上使用:
[assembly: CLSCompliant(true)]
您能够将特性应用于 CLSCompliantAttribute 下列程序元素:程序集、模块、类、结构、枚举、构造函数、方法、属性、字段、事件、接口、委托、参数和返回值。 可是,CLS 听从性的概念仅适用于程序集、模块、类型和类型的成员。
程序编译时默认不会检查代码是否符合 CLS 要求,可是若是你的能够是公开的(代码共享、Nuget 发布等),则建议使用使用 [assembly: CLSCompliant(true)]
,指明你的库符合 CLS 要求。
在团队开发中以及内部共享代码时,高质量的代码尤其重要,因此有必要使用工具检查代码,如 roslyn 静态分析、sonar 扫描等,也可使用上面的特性,自动使用 CLS 检查。
CLS 部分要求:
无符号类型不该成为该类的公共接口的一部分(私有成员可使用),例如 UInt32 这些属于 C# 的类型,但不是 CLS “标准” 中的。
指针等不安全类型不能与公共成员一块儿使用,就是公有方法中都不该该使用 unsafe 代码。(私有成员可使用)。
类名和成员名不该重名。虽然 C# 中区分大小写,可是 CLS 不建议同名非重载函数,例如 MYTEST 跟 Mytest。
只能重载属性和方法,不该重载运算符。重载运算符容易致使调用者不知情时出现程序错误,而且重载运算符要排查问题十分困难。
咱们能够编译如下代码,尝试使用 CLSCompliant
:
[assembly: CLSCompliant(true)] [CLSCompliant(true)] public class Test { public void MyMethod() { } public void MYMETHOD() { } }
IDE 中会警告:warning CS3005: 仅大小写不一样的标识符“Test.MYMETHOD()”不符合 CLS,编译时也会提示 Warn。固然,不会阻止编译,也不会影响程序运行。
总之,若是要标记一个程序集 CLS 规范,可使用 [assembly: CLSCompliant(true)]
特性。
[CLSCompliant(true)]
特性指示这个元素符合 CLS 规范,这时编译器或者 IDE 会检查你的代码,检查是否真的符合规范。
若是恰恰要写不符合规范的代码,则可使用 [CLSCompliant(false)]
。
C# 也能够定义类型别名。
using intbyte = System.Int32; using intkb = System.Int32; using intmb = System.Int32; using intgb = System.Int32; using inttb = System.Int32;
byte[] fileByte = File.ReadAllBytes("./666.txt"); intmb size = fileByte.Length / 1024;
一些状况下,使用别名能够提升代码可读性。真实项目不要使用以上代码,我只是写个示例,这并非合适的应用场景。
今天学习 Runtime 的代码就到这里为止。