Coding Style
A few years ago, I looked for resources regarding C# Coding Style. However, as time passed, these resources often became difficult to find—perhaps because the relevant blogs were shut down or Microsoft had revised their versions, leaving some less common definitions uncertain. Therefore, I started recording my notes in this article for my own easy reference.
Microsoft provides a package for code style analysis; you can refer to StyleCopAnalyzers on GitHub for the rules. However, when I write code, I only follow these rules loosely, still adhering primarily to my early programming habits or rules I encountered previously. For example, while other articles might suggest placing the opening brace on a new line, I actually place it on the same line. This is because my early PHP formatting rules were based on Java, where the opening brace is placed on the same line.
WARNING
The coding style defined in this article is merely my personal preference and does not necessarily represent the "best" way. It is best to follow your team's definitions or your own established habits.
Naming Rules
For naming, I follow C# casing rules completely. You can generally refer to the following two articles:
- StyleCopAnalyzers: I have summarized them as follows:
- Everything is Pascal Case, except for variables (Camel Case) and fields (depending on the situation).
- Fields are Camel Case, except in the following cases:
- Public and Internal fields use Pascal Case because they provide external access.
- Constants and Static Readonly fields use Pascal Case because they represent constants.
- Fields should not have prefixes like "", "m", or "s_".
- Interfaces must start with "I".
- For type parameters in generics, if there is only one and it can be any type, use "T". If there are multiple or specific requirements, use a word starting with "T", such as
List<T>andDictionary<TKey, TValue>.
- Casing Rules for Acronyms:
There are two types of abbreviations:
- Acronyms: Formed from the first letters of several words or phrases. For example, SHIELD is an acronym for Strategic Homeland Intervention, Enforcement and Logistics Division.
- Abbreviations: Formed by taking a few letters from a word.
- The casing of the first letter of an abbreviation depends on whether Pascal Case or Camel Case is used; subsequent letters are handled as follows:
- For three or more letters, whether it is an acronym or an abbreviation, the second letter onwards should be lowercase, e.g., "Sql" or "sql".
- For two-letter acronyms, they are all uppercase or all lowercase, e.g., "IO" or "io".
- For two-letter abbreviations, the second letter onwards is lowercase, e.g., "Id" or "id".
You can also refer to the article by Darkthread: "The Confusion of Acronym Casing: LINQHelper or LinqHelper?".
WARNING
Although later versions of MSDN removed the casing rules for abbreviations, they still follow these rules during development. It is worth noting that this abbreviation rule only applies to C#; in reality, many languages have different definitions for abbreviations. For example, in the JavaScript DOM, acronyms are always all uppercase or all lowercase, such as innerHTML.
Ordering Rules
In StyleCopAnalyzers, rules starting with SA12 are related to ordering. Except for methods, I generally follow these rules.
- SA1201: Member ordering.
- SA1202: Access Modifier ordering:
- public
- internal
- protected internal
- protected
- private protected
- private
- SA1203: Constant fields must come before non-constant fields.
- SA1204: Static members must be placed above non-static members of the same type.
- SA1206, SA1207: DeclarationKeyword ordering:
- Access Modifiers 1. protected 2. internal
- static
- Other Keywords
- SA1208, SA1210: When using namespaces, those starting with
System.should be placed first, and the rest should be sorted alphabetically. - SA1208, SA1210, SA1209, SA1211, SA1216, SA1217: Using ordering rules are as follows:
- Using Namespaces: Among namespaces, those starting with
System.are placed first, and the rest are sorted alphabetically. - Using Static: Sorted by the full type name.
- Using Alias: Sorted by the alias name.
- Using Namespaces: Among namespaces, those starting with
- Aliases must be placed after Using Namespaces, and sorted by the alias name.
- SA1212: Property and Indexer getters must be placed before setters.
- SA1213: Event add accessors must be placed before remove accessors.
- SA1214: Readonly fields must be placed before non-readonly fields.
Complete Example
using Namespace;
using static Namespace.StaticClassName;
using Namespace = Alias;
public class ClassName {
// I only list these because I don't set Protected Fields and Public Non-static (Const) Fields
#region Fields
public const int ConstantName = 0;
internal const int InternalConstantName = 0;
private const int PrivateConstantName = 0;
public readonly static int ReadonlyStaticFieldName = 0;
private static int StaticFieldName = 0;
private int fieldName = 0;
#endregion
#region Constructors
static ClassName() { }
public ClassName() { }
protected ClassName() { }
private ClassName() { }
#endregion
~ClassName() { }
// I only design Public Delegates
public delegate int Delegate(int x);
// I only design Public Events
public event EventHandler Event;
// I don't design private Properties
#region Properties
public int PropertyName { get; set; }
internal int InternalPropertyName { get; set; }
protected internal int ProtectedInternalPropertyName { get; set; }
protected int ProtectedPropertyName { get; set; }
#endregion
// I don't design private Indexers
#region Indexers
public int this[byte i] { get; set; }
internal int this[short i] { get; set; }
protected internal int this[int i] { get; set; }
protected int this[long i] { get; set; }
#endregion
// The following is the ordering under normal, unrelated conditions
#region Methods
public static void StaticMethodName() { }
internal static void InternalStaticMethodName() { }
public void MethodName() { }
internal void InternalMethodName() { }
protected internal void ProtectedInternalMethodName() { }
protected void ProtectedMethodName() { }
private void PrivateMethodName() { }
#endregion
public static bool operator ==(ClassName left, ClassName right) {
return left == right;
}
}TIP
The code above uses
regionfor readability of the sample. In practice, I don't useregionto categorize code; I only use it to hide details I don't think developers need to see, allowing them to focus on other relevant code.Regarding method ordering, I do not follow the SA1202 (Access Modifiers) rule. Instead, I follow a rule I saw earlier: grouping homogeneous methods together. This way, when I see a method calling other methods, I can just scroll down to see the relevant implementation. Example:
Ordering for methods called only once.
csharppublic void Method() { MethodA(); MethodB(); } private void MethodA() { MethodA1(); } private void MethodA1() { } private void MethodB() { }Ordering for methods called repeatedly: Private methods are placed below the first calling method. If multiple similar methods call it, consider placing it below the last calling method.
csharppublic void MethodA() { SubMethod(); } private void SubMethod() { } public void MethodB() { SubMethod(); } public void MethodC() { SubMethod(); } // OR public void MethodA1() { SubMethod1(); } public void MethodB1() { SubMethod1(); } public void MethodC1() { SubMethod1(); } private void SubMethod1() { }
I do not sort private methods according to the SA1204 (Static and Non-static) rule because, as mentioned above, I group related methods together. Whether I mark a private method as static largely depends on whether it uses instance members. It is possible that a private method originally used other private methods and wasn't marked static, but after refactoring to pass parameters, it became static.
Comments
Single-line Comments
- Format: Start with "//" followed by a space, then the comment, e.g.,
// Your Comment.
TIP
Although this comment format is one I encountered when I first started programming, most modern conventions don't strictly require it. However, within Microsoft, they still use this format, as seen in their source code or MSDN examples.
- Position: Can appear above or to the right of the code.
- Purpose: As a "Clean Code" advocate, I prefer using comments to explain why something is being done, rather than documenting what the code is doing.
Documentation Comments
When using Visual Studio, typing three "/" on a class, struct, interface, or member generates XML-structured documentation comments. These comments contain specific XML tags used to explain code and provide API documentation. For more details, refer to Documentation Comments.
If you need to tag generic types, you can use "{}" instead of "<>" because "<" and ">" might be misinterpreted in an XML structure. Example:
/// <seealso cref="Dictionary{TKey, TValue}"/>Private members do not need documentation comments because they are not part of the API and are not exposed to other programs.
Task List
The compiler usually processes special comment keywords. For example, Visual Studio provides "HACK", "TODO", "UNDONE", and "UnresolvedMergeConflict" by default. Comments containing these keywords appear in the Visual Studio Task List window to remind developers of pending tasks. Usage scenarios:
- TODO: Used to mark features or items that need to be completed or implemented. Usually used when you encounter something that needs to be done but don't have time for it at the moment.
- UNDONE: Used to mark features or tasks that are not yet finished. Usually used when you are in the middle of development and need to continue later.
- HACK: A keyword used to mark code blocks that need modification or correction. Usually a temporary solution added to solve a problem quickly. Once the issue is resolved, the code must be modified, and this keyword removed.
- UnresolvedMergeConflict: Related to version control conflicts; usually doesn't need to be added manually, as it is handled automatically by the version control system.
TIP
The meanings of "TODO" and "UNDONE" differ slightly: "TODO" usually indicates work that needs to be done in the future, while "UNDONE" usually indicates work that is in progress but not yet finished.
Formatting
Left Braces
As mentioned earlier, since I preferred placing them on the same line when I started with PHP, I do the same for C#. Placing them on a new line feels unnatural. Keywords like else, catch, and finally are placed on the same line. Example:
if () {
} else if () {
} else {
}
try {
} catch {
} finally {
}Spacing
The spacing I use in code is generally the same as the Visual Studio default. I usually insert spaces in the following situations:
- After a ",", unless the "," is at the end of a line.
- After keywords in control flow statements.
- Before and after the colon for base classes or interfaces in class declarations.
- After the ";" in
forstatements. - Before and after operators, except for "++" and "--". When using "++" and "--", no space is added between them and the variable.
Example:
for (int i = 0; i < 10; i++) {
}
Math.Max(1, 2);Operator Line Breaks
When writing essays, it is generally recommended not to place punctuation at the beginning of a line. Instead, you should leave the end of the line empty and move the word to the start of the new line before adding punctuation. Similarly, binary operators in mathematical expressions are easier to read when placed at the end. This was my initial formatting style, but later I saw someone suggest that when formatting mathematical formulas, placing binary operators at the beginning of a new line is easier to read. According to ChatGPT, this style is called "out-of-line style," but I can no longer find the relevant article. It is worth noting that programming languages have similar conventions, such as the Google Java Style Guide. Quoting the relevant section:
The prime directive of line-wrapping is: prefer to break at a higher syntactic level. Also:
When a line is broken at a non-assignment operator the break comes before the symbol. (Note that this is not the same practice used in Google style for other languages, such as C++ and JavaScript.)
This also applies to the following "operator-like" symbols:
- the dot separator (.)
- the two colons of a method reference (:😃
- an ampersand in a type bound (<T extends Foo & Bar>)
- a pipe in a catch block (catch (FooException | BarException e)).
When a line is broken at an assignment operator the break typically comes after the symbol, but either way is acceptable.
This also applies to the "assignment-operator-like" colon in an enhanced for ("foreach") statement. 3. A method or constructor name stays attached to the open parenthesis (() that follows it. 4. A comma (,) stays attached to the token that precedes it. 5. A line is never broken adjacent to the arrow in a lambda, except that a break may come immediately after the arrow if the body of the lambda consists of a single unbraced expression.
Overall, my formatting style aligns with the essay convention for assignment operators (=) and binary operators: assignment operators do not move to the new line, while binary operators do. However, when a Lambda arrow is at a line break, I move it to the new line (unlike point 5 in the quote). This is partly because the code I've seen is formatted this way, and partly to distinguish it from "=", making it easier for me to tell if it's a field or a Lambda-based property.
Example:
class Test {
// "=" at the line break, does not move to the new line
private string field =
"Field";
// "=>" at the line break, moves to the new line for distinction
public string Property
=> "Property";
public void Method() {
// "||" at the line break, moves to the new line
if (condition1
|| condition1
) {
}
// Binary operator at the line break, moves to the new line
int a = b
+ c;
}
}TIP
Note that the above is just a demonstration; in reality, given the length and complexity of the example code, line breaks are not necessary.
Ternary Operators
For short, non-nested ternary operators, you can keep them on a single line, e.g.:
bool result = condition ? result1 : result2;If it is too long, you need to break the line. You can use one of the following two methods:
bool result = condition1
? result1 : result2;
bool result = condition1
? result1
: result2;For nested ones, you must break the line. Refer to the following:
// Try to place sub-conditions in the ":" part to improve readability
bool result = condition1
? result1
: condition2
? result2
: condition3
? result3
: result4;
// Another nested ternary operator formatting style seen in ASP.NET Core source code
bool result = condition1 ? result1
: condition2 ? result2
: condition3 ? result3
: result4;The two nested ternary operators written as if-else look roughly like this:
if (condition1) {
result = result1;
} else {
if (condition2) {
result = result2;
} else {
if (condition3) {
result = result3;
} else {
result = result4;
}
}
}
if (condition1) {
result = result1;
} else if (condition2) {
result = result2;
} else if (condition3) {
result = result3;
} else {
result = result4;
}TIP
As an aside, some programming languages like PHP do provide an elseif keyword, but in C#, else if is just a result of formatting.
Indentation
No explanation needed: I am a "spaces" person. I generally use 4 spaces, and 2 spaces for XML.
Blank Lines
- No blank lines between fields; add one blank line between other members. This not only improves readability but also allows Visual Studio to display separator lines between them if configured.
- No blank lines at the beginning or end of blocks.
- At most one blank line at a time.
- Leave one blank line after the code ends.
Example:
namespace namespace1 {
// No blank line between Namespace and Class
public class Class1 {
// No blank line between Class and Field
private string field;
public string Property1 { get; set; }
public string Property2 { get; set; }
public void Method() {
}
}
}
// Add a blank line after the code endsTIP
In Unix/Linux environments, files use a Line Feed (LF) as the line terminator. When an editor opens a file ending with a line feed, if the last line doesn't have one, the editor treats it as an incomplete line. This can cause issues, such as the last line of text being hidden in some editors or not being correctly marked in some version control systems. To avoid these issues, Visual Studio's default formatting and some code conventions add a blank line at the end of the file.
Code Length
I am accustomed to setting 80, 100, and 120-character guide lines in Visual Studio. These help me evaluate whether code needs to be broken to enhance readability.
TIP
In the early days, due to the limitations of many terminals and text editors, code length was usually restricted to 80 characters. However, with the advent of larger modern screens and improved text editors, code length is no longer strictly limited. Nevertheless, to increase readability, I still try to avoid writing excessively long code. This also helps avoid the need to constantly scroll to read a whole block of code when developing on smaller screens like laptops.
Change Log
- 2022-11-06 Initial version created.