Table of Contents:
-
File Operations
-
Debug and Trace Classes
-
Conditional Compilation
-
MethodImpl Attribute
-
CLSComplianceAttribute
-
Custom Type Aliases When Necessary
I have recently been reading the source code of .NET Core Runtime, referencing the code of experts to learn coding techniques and improve my coding skills. During the learning process, I have documented insights and code snippets that are worth applying to projects for future reference.
1. File Operations
This code is found under System.Private.CoreLib
, which refines the code in System.IO.File for use by the CLR.
When using files, it is important to check whether the file path exists in advance. There are many places where files are used in daily projects, so a uniform method to check if a file exists is beneficial:
public static bool Exists(string? path)
{
try
{
// string? can be changed 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 advisable to convert paths to absolute paths when performing final processing in the project:
Path.GetFullPath(path)
Of course, relative paths will be correctly recognized by .NET, but for operational debugging and various considerations, absolute paths make it easier to locate specific positions and troubleshoot issues.
When writing code, use relative paths to maintain flexibility; convert them to absolute paths during runtime.
The exceptions like NotSupportedException
listed above are various exceptions that may occur during file operations. These exceptions can commonly arise in cross-platform applications; handling these exception types in advance can optimize file processing logic and facilitate error handling.
2. Reading Files
This code resides in System.Private.CoreLib.
There is a method to read a file into a 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, and if you simply need to read file content, you can refer to the following code:
FileStream fs = new FileStream(path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 1)
The above code also corresponds with File.ReadAllBytes
, which internally uses InternalReadAllBytes
to handle document reading:
private static byte[] InternalReadAllBytes(String path, bool checkHost)
{
byte[] bytes;
// This FileStream constructor is not public, 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)
__Error.EndOfFile();
index += n;
count -= n;
}
}
return bytes;
}
This indicates that we can safely use the functions within the File
static class, as it has already handled some logic and automatically releases files.
If we manually instantiate FileStream
, we need to check certain conditions to avoid runtime errors. It’s best to refer to the above code.
The default buffer size for .NET file streams is 4096
bytes:
internal const int DefaultBufferSize = 4096;
This code is defined in the File class, and developers cannot set the cache block size; 4k is generally the optimal block size.
The file size limit for ReadAllBytes
is 2 GB.
3. Debug and Trace Classes
The namespaces for these two classes are System.Diagnostics
. Debug and Trace provide a set of methods and properties that assist in debugging code.
All functions in Debug are not effective in Release mode, and all output streams will not display on the console; listeners must be registered to read these streams.
Debug can print debug information and use assertions to check logic, which makes code more reliable without impacting the performance and size of the shipped product.
Output methods such as Write, WriteLine, WriteIf, and WriteLineIf do not print directly to the console.
To print debug information to the console, you can register a listener:
ConsoleTraceListener console = new ConsoleTraceListener();
Trace.Listeners.Add(console);
Note that starting from .NET Core 2.x, Debug does not have Listeners since Debug uses Trace's listeners.
We can register listeners for Trace, which equivalently sets listeners for Debug.
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
Debug.WriteLine("aa");
Listeners in .NET Core inherit from TraceListener, such as TextWriterTraceListener, ConsoleTraceListener, and DefaultTraceListener.
If you need to output to a file, you can inherit TextWriterTraceListener
and write the file stream output, or you can use DelimitedListTraceListener.
Example:
TraceListener listener = new DelimitedListTraceListener(@"C:\debugfile.txt");
// Add listener.
Debug.Listeners.Add(listener);
// Write and flush.
Debug.WriteLine("Welcome");
To format the output stream, you can use the following properties to control the layout:
| Property | Description |
| ------------ | --------------------------------------------------------- |
| AutoFlush | Gets or sets a value indicating whether to call Flush() on Listeners after each write. |
| IndentLevel | Gets or sets the level of indentation. |
| IndentSize | Gets or sets the number of spaces for indentation. |
// 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
The .Assert()
method is very helpful for debugging programs, sending a strong message to developers. In the IDE, assertions interrupt the normal operation of the program without terminating the application.
The most intuitive effect of .Assert()
is to output the location of the program assertion.
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.Print()
can also output information, behaving similarly to the C language printf
function, writing messages followed by a line ending, with the default line terminator being a carriage return followed by a newline.
When running programs in the IDE with methods like Debug.Assert()
and Trace.Assert()
, if the condition is false, the IDE will assert, which is equivalent to a conditional breakpoint.
In non-IDE environments, the program will output some information but won’t have a breakpoint effect.
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
I believe we can introduce Debug and Trace into projects to work alongside logging components. Debug and Trace are used to log diagnostic information about program execution, which is helpful for troubleshooting program issues later; logging is used to record business processes, data information, etc.
The principle of .Assert()
is that it does nothing when true; it calls the Fail function when false; and unless you register a listener, the default behavior is also to do nothing.
The only thing .Assert()
can do is execute the Fail method when the condition is false, and we can also manually call the Fail method directly. The code for Fail is as follows:
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. Conditional Compilation
#if
conditional compilation hides non-conditional (#else if
) code, which we may easily overlook during development. When switching the conditional constant to this part of the code, various reasons might cause errors.
If attributes are used to mark code for conditional compilation, attention can be drawn to this part of the code during development.
[Conditional("DEBUG")]
For example, when modifying all references—changing a class member variable or a static variable name—the code in the non-conditional part of #if
will not be altered because this code is “inactive,” whereas the code marked with [Conditional("DEBUG")]
will remain synchronized regardless of conditions.
Methods marked with the Conditional
attribute, stay valid during development and can be excluded during compilation.
Code snippets can only use #if
; if it is a single method, you can use Conditional
.
5. MethodImpl Attribute
This attribute is found in the System.Runtime.CompilerServices namespace and specifies how to implement methods in detail.
For the usage of inline functions, refer to https://www.whuanle.cn/archives/995.
The MethodImpl attribute can influence the behavior of the JIT compiler.
.
It is not possible to use MemberInfo.GetCustomAttributes
to retrieve information about this attribute, meaning that related information about MethodImpl
cannot be obtained through the method of getting attributes (reflection). Instead, you can only call MethodInfo.GetMethodImplementationFlags()
or ConstructorInfo.GetMethodImplementationFlags()
to retrieve it.
MethodImpl
can be used on methods and constructors.
MethodImplOptions
is used to set the compilation behavior, and the enumeration values can be combined. The enumeration descriptions are as follows:
| Enumeration | Value | Description |
| ---------------------- | ------ | ------------------------------------------------------------ |
| AggressiveInlining | 256 | The method should be inlined if possible. |
| AggressiveOptimization | 512 | This method contains a hot path and should be optimized. |
| ForwardRef | 16 | The method is declared, but the implementation is provided elsewhere. |
| InternalCall | 4096 | This call is an internal call, meaning it calls a method implemented in the common language runtime. |
| NoInlining | 8 | The method cannot be inlined. Inlining is an optimization technique that replaces method calls with the method body. |
| NoOptimization | 64 | When debugging potential code generation issues, this method is not optimized by the just-in-time (JIT) compiler or native code generation (see Ngen.exe). |
| PreserveSig | 128 | Exports the method signature exactly as declared. |
| Synchronized | 32 | This method can only be executed by one thread at a time. Static methods lock on the type, while instance methods lock on the instance. Only one thread can execute any instance function, and only one thread can execute any static function of any class. |
| Unmanaged | 4 | This method is implemented in unmanaged code. |
The method marked with Synchronized
can avoid some issues in multithreading, but it is not recommended to lock instances or types on public types, because Synchronized
can lock public types and instances in code that is not your own. This may lead to deadlocks or other synchronization issues.
This means that if a shared member has already set a lock, you should not use it again in a Synchronized
method, as double locking can easily lead to deadlocks and other issues.
5. CLSCompliantAttribute
Indicates whether the program element complies with the Common Language Specification (CLS).
You can refer to the CLS specification at:
https://docs.microsoft.com/en-us/dotnet/standard/language-independence
https://www.ecma-international.org/publications/standards/Ecma-335.htm
Global enabling method:
Add an AssemblyAttribytes.cs
file in the project directory, or open the obj
directory and find the file ending with AssemblyAttributes.cs
, such as .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
, and add:
using System; // Do not add this line if it already exists
[assembly: CLSCompliant(true)]
After that, you can use the [CLSCompliant(true)]
attribute in your code.
Local enabling:
You can also apply it to class members, etc.:
[assembly: CLSCompliant(true)]
You can apply the CLSCompliantAttribute
to the following program elements: assembly, module, class, struct, enum, constructor, method, property, field, event, interface, delegate, parameter, and return value. However, the concept of CLS compliance only applies to assemblies, modules, types, and type members.
The compiler does not check the code against CLS requirements by default. However, if your code is public (for code sharing, NuGet publishing, etc.), it is recommended to use [assembly: CLSCompliant(true)]
to indicate that your library complies with 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 attribute to automatically perform CLS checks.
CLS compliance requirements:
-
Unsigned types should not be part of the public interface of the class (private members can use them), such as
UInt32
, which belong to C# types but are not part of the CLS "standard." -
Unsafe types, like pointers, cannot be used with public members, meaning you should not use unsafe code in public methods (private members can use it).
-
Class names and member names should not share the same name. Although C# is case-sensitive, CLS does not recommend having non-overloaded functions with names that differ only in case, such as
MYTEST
andMytest
. -
Only properties and methods should be overloaded; operators should not be overloaded. Overloading operators can lead to program errors without the caller's knowledge, and debugging issues with overloaded operators can be very difficult.
We can compile the following code to try using CLSCompliant
:
[assembly: CLSCompliant(true)]
[CLSCompliant(true)]
public class Test
{
public void MyMethod()
{
}
public void MYMETHOD()
{
}
}
The IDE will warn: warning CS3005: The identifier “Test.MYMETHOD()” only differs by case and does not comply with CLS; a warning will also be prompted during compilation. Of course, it will not prevent compilation, nor will it affect program execution.
In summary, if you want to mark an assembly as CLS-compliant, you can use the [assembly: CLSCompliant(true)]
attribute.
The [CLSCompliant(true)]
attribute indicates that this element complies with CLS specifications, at which point the compiler or IDE will check your code to see if it genuinely complies with the specifications.
If you want to write code that does not comply with the specifications, you can use [CLSCompliant(false)]
.
6. Custom Type Aliases When Needed
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 am just providing an example, and this is not an appropriate application scenario.
Today’s study of the Runtime code ends here.
文章评论