C# good code study notes (1): file operation, reading file, Debug/Trace class, Conditional conditional compilation, CLS

2020年12月16日 2037点热度 0人点赞 0条评论

table of Contents:

  1. File operations

  2. Debug, Trace category

  3. Conditional compilation

  4. MethodImpl feature

  5. CLSComplianAttribute

  6. If necessary, customize the type alias

Recently, I am reading the source code of .NET Core Runtime, refer to the code of the boss, learn writing skills and improve the code level. During the learning process, the learning experience and code fragments worth applying to the project are recorded for future reference.

1, File operation

This code is under System.Private.CoreLib to streamline the code in System.IO.File for use by CLR.

When using files, it is necessary to determine in advance whether the file path exists. There should be many places where files are used in daily projects. You can unify a method to determine whether the file exists:

        public static bool Exists(string? path)
                // You can change string? to 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;

It is recommended that when the path is finalized in the project, it should be converted to an absolute path:


Of course, the relative path will be correctly identified by .NET, but for the operation and maintenance troubleshooting and various considerations, the absolute path is easy to locate the specific location and troubleshoot.

When writing code, use a relative path, don't write it hard, and improve flexibility; convert it to an absolute path during the runtime;

The above NotSupportedException and other exceptions are various exceptions that may occur in operating files. For cross-platform applications, these exceptions may be very common. Identifying and processing the exception types in advance can optimize file processing logic and facilitate Screening processing error.

2, read the file

This code is in System.Private.CoreLib.

There is a method to read a file and convert it to byte[] as follows:

        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;

You can see the use of FileStream. If you simply read the content of a file, you can refer to the code inside:

        FileStream fs = new FileStream(path,
                                       bufferSize: 1)

The above code also has File.ReadAllBytes corresponding to it. File.ReadAllBytes uses InternalReadAllBytes internally to process document reading:

        private static byte[] InternalReadAllBytes(String path, bool checkHost)
            byte[] bytes;
            // The constructor of this FileStream is not public and developers cannot use it
            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)
                    index += n;
                    count -= n;
            return bytes;

This paragraph shows that we can use the functions in the File static class with confidence, because some logic has been processed inside and the file is automatically released.

If we manually new FileStream, we need to judge some situations so as not to report an error when using it. It is best to refer to the above code.

The default .NET file stream cache size is 4096 bytes:

internal const int DefaultBufferSize = 4096;

This code is defined in the File class. Developers cannot set the size of the cache block. In most cases, 4k is the optimal block size.

The maximum file size of ReadAllBytes is 2 GB.

3, Debug, Trace category

The namespace of these two classes is System.Diagnostics. Debug and Trace provide a set of methods and properties that help debug code.

All functions in Debug will not be valid in Release, and all output streams will not be displayed on the console. You must register a listener to read these streams.

Debug can print debugging information and use assertion checking logic to make the code more reliable without affecting the performance and code size of the shipped product.

Such output methods include Write, WriteLine, WriteIf and WriteLineIf, etc. The output in this will not be directly printed to the console.

If you need to print debugging information to the console, you can register a listener:

ConsoleTraceListener console = new ConsoleTraceListener();

Note that Debug above .NET Core 2.x does not have Listeners because Debug uses Trace listeners.

We can register listeners for Trace.Listeners, which this is equivalent to setting the listener relative to Debug.

        Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));

The listeners in .NET Core inherit TraceListener, such as TextWriterTraceListener, ConsoleTraceListener, DefaultTraceListener.

If you need to output to a file, you can inherit TextWriterTraceListener yourself to write file stream output, or you can use DelimitedListTraceListener.


TraceListener listener = new DelimitedListTraceListener(@"C:\debugfile.txt");

        // Add listener.

        // Write and flush.

To process the above method output console, you can also use

ConsoleTraceListener console=...

// equal to
var console = new TextWriterTraceListener(Console.Out)

In order to format the output stream, you can use the following attributes to control the layout:

Properties Description
AutoFlush Gets or sets a value that indicates whether Listeners should be called on Flush() after each write.
IndentLevel Get or set the indent level.
IndentSize Gets or sets the number of spaces for indentation.
        // 1.

        // Indent and then unindent after writing.

        // End.

        // Sleep.

The .Assert() method is very helpful for us to debug the program. Assert sends a strong message to the developer. In the IDE, assertions will interrupt the normal operation of the program, but will not terminate the application.

The most intuitive effect of .Assert() is to output the assertion position of the program.

        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.");
---- 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() can also output information. It has the same behavior as the printf function of C language. It writes the message followed by a line ending character. The default line ending character is a carriage return followed by a newline character.

When running the program in the IDE, use methods such as Debug.Assert(), Trace.Assert(), etc. When the condition is false, the IDE will assert, which is equivalent to a conditional breakpoint.

In a non-IDE environment, the program will output some information, but there will be no interruption effect.

        Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
Process terminated. Assertion Failed
   at Program.Main(String[] args) in C:\ConsoleApp4\Program.cs:line 44

I personally think that Debug and Trace can be introduced into the project and used in conjunction with the log component. Debug and Trace are used to record the diagnostic information of program operation, which is convenient for troubleshooting program problems in the future; the log is used to record business processes, data information, etc.

The principle of .Assert() is to do nothing when true; call the Fail function when false; if you don't register a listener, there is nothing to do by default.

The only thing that .Assert() can do is to wait for the condition to be false and execute the Fail method. Of course, we can also manually call the Fail method directly. The Fail code is as follows:

public static void Fail(string message) {
            if (UseGlobalLock) {
                lock (critSec) {
                    foreach (TraceListener listener in Listeners) {
                        if (AutoFlush) listener.Flush();
            else {
                foreach (TraceListener listener in Listeners) {
                    if (!listener.IsThreadSafe) {
                        lock (listener) {
                            if (AutoFlush) listener.Flush();
                    else {
                        if (AutoFlush) listener.Flush();

4, conditional compilation

#if Conditional compilation will hide the non-conditional (#else if) code. We may ignore this part of the code in our development. When we switch the conditional constant to this part of the code, it is likely to cause an error for various reasons.

If you use the feature to mark for conditional compilation, you can notice this part of the code during the development process.


For example, when using modify all references-modify a class member variable or static variable name, the code in #if non-conditional will not be modified, because this part of the code is "invalid" and uses [Conditional("DEBUG")] The code has nothing to do with the conditions and will be modified synchronously.

The methods of Conditional feature marking, etc., remain valid during the development process, and may be excluded when compiling.

Code snippets can only use #if, if it is a single method, you can use Conditional.

5, MethodImpl feature

This feature is in the System.Runtime.CompilerServices namespace and specifies the details of how to implement the method.

For the usage of inline functions, please refer to https://www.whuanle.cn/archives/995

The MethodImpl feature can affect the behavior of the JIT compiler.

It is not possible to use MemberInfo.GetCustomAttributes to obtain the information of this characteristic, that is, the information related to MethodImpl (reflection) cannot be obtained through the method of obtaining the characteristics, and only MethodInfo.GetMethodImplementationFlags() or ConstructorInfo.GetMethodImplementationFlags () can be called To retrieve.

MethodImpl can be used on methods and constructors.

MethodImplOptions is used to set the compilation behavior. Enumeration values can be used in combination. The enumeration description is as follows:

Enumeration Enumeration value Description
AggressiveInlining 256 This method should be inlined if possible.
AggressiveOptimization 512 This method contains a hot path and should be optimized.

| ForwardRef | 16 | The method has been declared, but the implementation is provided elsewhere.|
| InternalCall | 4096 | The call is an internal call, which means it calls a method implemented in the common language runtime. |
| NoInlining | 8 | The method cannot be an inline method. Inlining is an optimization method by which method calls are replaced with method bodies. |
| NoOptimization | 64 | When debugging possible code generation issues, this method is not optimized by just-in-time (JIT) compilers or native code generation (see [Ngen.exe](https://docs.microsoft.com/zh-cn /dotnet/framework/tools/ngen-exe-native-image-generator)). |
| PreserveSig | 128 | Export method signature exactly as declared. |
| Synchronized | 32 | This method can only be executed on one thread at a time. Static methods are locked on the type, while instance methods are locked on the instance. Only one thread can be executed in any instance function, and only one thread can be executed in a static function of any class. |
| Unmanaged | 4 | This method is implemented in unmanaged code. |

The modified method of Synchronized can avoid some problems in multi-threading, but it is not recommended to use lock instances or type locks for public types, because Synchronized can lock public types and instances of non-self code. This may cause deadlocks or other synchronization issues.

This means that if the shared member has already set a lock, it should not be used in the Synchronized method anymore. This double lock can easily lead to deadlock and other problems.

6, CLSCompliantAttribute

Indicates whether the program element complies with the Common Language Specification (CLS).

CLS specification can refer to:



Global opening method:

Add an AssemblyAttribytes.cs file under the program directory, or open the obj directory and find the file ending in AssemblyAttributes.cs, such as .NETCoreApp,Version=v3.1.AssemblyAttributes.cs, and add:

using System; // Don't add if this line already exists
[assembly: CLSCompliant(true)]

Then you can use the [CLSCompliant(true)] feature in your code.

Partially open:

It can also be used on members such as classes:

[assembly: CLSCompliant(true)]

You can apply attributes to the following program elements of CLSCompliantAttribute: assembly, module, class, structure, enumeration, constructor, method, attribute, field, event, interface, delegate, parameter, and return value. However, the concept of CLS compliance only applies to assemblies, modules, types, and type members.

When the program is compiled, it will not check whether the code meets the CLS requirements by default, but if yours can be public (code sharing, Nuget release, etc.), it is recommended to use [assembly: CLSCompliant(true)] to indicate that your library meets CLS requirements.

In team development and when sharing code internally, high-quality code is particularly important, so it is necessary to use tools to check the code, such as roslyn static analysis, sonar scanning, etc. You can also use the above features to automatically use CLS checks.

Part of the CLS requirements:

  1. Unsigned types should not be part of the public interface of the class (private members can be used), such as UInt32 These are C# types, but not in the CLS "standard".

  2. Unsafe types such as pointers cannot be used with public members, that is, unsafe code should not be used in public methods. (Private members can be used).

  3. The class name and member name should not be the same. Although C# is case sensitive, CLS does not recommend non-overloaded functions with the same name, such as MYTEST and Mytest.

  4. Only attributes and methods can be overloaded, not operators. Overloaded operators can easily lead to program errors when the caller does not know it, and it is very difficult to troubleshoot the overloaded operators.

We can compile the following code and try to use CLSCompliant:

[assembly: CLSCompliant(true)]
public class Test
    public void MyMethod()
    public void MYMETHOD()

There will be a warning in the IDE: warning CS3005: The identifier "Test.MYMETHOD()" with only case difference does not conform to CLS, and Warn will also be prompted during compilation. Of course, it will not prevent compilation, nor will it affect program operation.

In short, if you want to mark an assembly CLS specification, you can use the [assembly: CLSCompliant(true)] feature.

The [CLSCompliant(true)] feature indicates that this element complies with the CLS specification. At this time, the compiler or IDE will check your code to check whether it really complies with the specification.

If you want to write code that does not meet the specifications, you can use [CLSCompliant(false)].

7, if necessary, customize the type alias

C# can also define type aliases.

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;

In some cases, using aliases can improve code readability. Do not use the above code in real projects, I just write an example, this is not a suitable application scenario.

The code for learning Runtime today ends here.