zoukankan      html  css  js  c++  java
  • C# 好代码学习笔记(1):文件操作、读取文件、Debug/Trace 类、Conditional条件编译、CLS

    目录:

    1,文件操作

    2,Debug、Trace类

    3,条件编译

    4,MethodImpl 特性

    5,CLSComplianAttribute

    6,必要时自定义类型别名

    最近在阅读 .NET Core Runtime 的源码,参考大佬的代码,学习编写技巧和提高代码水平。学习过程中将学习心得和值得应用到项目中的代码片段记录下来,供日后查阅。

    1,文件操作

    这段代码在 System.Private.CoreLib 下,对 System.IO.File 中的代码进行精简,供 CLR 使用。

    当使用文件时,要提前判断文件路径是否存在,日常项目中要使用到文件的地方应该不少,可以统一一个判断文件是否存在的方法:

            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 等异常是操作文件中可能出现的各种异常情况,对于跨平台应用来说,这些异常可能都是很常见的,提前将其异常类型识别处理,可以优化文件处理逻辑以及便于筛查处理错误。

    2,读取文件

    这段代码在 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。

    3,Debug 、Trace类

    这两个类的命名空间为 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:ConsoleApp4Program.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();
                        }
                    }            
                }
            }        
    

    4,条件编译

    #if 条件编译会隐藏非条件(#else if)代码,我们开发中很可能会忽略掉这部分代码,当我们切换条件常量到这部分代码时,很可能因为各种原因导致报错。

    如果使用特性进行条件编译标记,在开发过程中就可以留意到这部分代码。

    [Conditional("DEBUG")]
    

    例如,当使用修改所有引用-修改一个类成员变量或者静态变量名称时,#if 非条件中的代码不会被修改,因为这部分代码“无效”,而且使用 [Conditional("DEBUG")] 的代码则跟条件无关,会被同步修改。

    Conditional 特性标记的方法等,在开发过程中保持有效,当在编译时可能被排除。

    代码片段只能使用 #if 了,如果是单个方法,则可以使用 Conditional

    5,MethodImpl 特性

    此特性在 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 方法中使用,这样双重锁定容易导致死锁以及其他问题。

    5,CLSCompliantAttribute

    指示程序元素是否符合公共语言规范 (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 部分要求:

    1. 无符号类型不应成为该类的公共接口的一部分(私有成员可以使用),例如 UInt32 这些属于 C# 的类型,但不是 CLS “标准” 中的。

    2. 指针等不安全类型不能与公共成员一起使用,就是公有方法中都不应该使用 unsafe 代码。(私有成员可以使用)。

    3. 类名和成员名不应重名。虽然 C# 中区分大小写,但是 CLS 不建议同名非重载函数,例如 MYTEST 跟 Mytest。

    4. 只能重载属性和方法,不应重载运算符。重载运算符容易导致调用者不知情时出现程序错误,并且重载运算符要排查问题十分困难。

    我们可以编译以下代码,尝试使用 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)]

    6,必要时自定义类型别名

    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 的代码就到这里为止。

  • 相关阅读:
    leetcode链表--7、rotate-list(将单链表从右数第k个结点以及之后结点移动到链表最前头)
    10、剑指offer--矩形覆盖
    leetcode链表--6、linked-list-cycle-ii(有环单链表环的入口结点)
    9、剑指offer--变态跳台阶
    8、剑指offer--跳台阶
    leetcode链表--5、linked-list-cycle(单链表是否有环)
    7、剑指offer--斐波那契数列
    Node.js 学习(二) 创建第一个应用
    Node.js 学习(一) 安装配置
    JavaScript 函数调用
  • 原文地址:https://www.cnblogs.com/whuanle/p/14141213.html
Copyright © 2011-2022 走看看