Backend Encoding Specification
To be a qualified developer, the most basic quality is to follow coding specifications. We are taught from a young age that "handwriting reflects the person," and writing code is the same. Good coding style demonstrates personal professional literacy. Good coding standards help us collaborate better as a team:
- Facilitate code communication and maintenance
- Do not affect coding efficiency, do not conflict with general habits
- Make code more beautiful and easier to read
- Make code logic clearer and easier to understand
Specification Purpose
In the lifecycle of a software, 80% of the cost lies in maintenance. Almost no software is maintained by the original developer throughout its entire lifecycle.
The core values of coding specifications are:
- Improve software readability, allowing programmers to understand new code as quickly and thoroughly as possible
- Standardize application structure and coding style, facilitating team collaboration and knowledge inheritance
- Make source code rigorous, highly readable, and clear in meaning
- Consistent with other language conventions and as intuitive as possible
To fully leverage the value of coding specifications, every software developer must consistently abide by them.
Qualities of High-Quality Code
High-quality code should possess the following core qualities:
- Understandability: Code must be easy to read, simple, and clear, highlighting the key points. Code should be easy to reuse, free of redundant code, and accompanied by corresponding documentation.
- Correctness: Code must correctly implement its intended functions, be fully tested, and compile and run correctly according to the documentation.
- Consistency: Code should follow unified programming styles and design principles, ensuring consistent style across different codes to facilitate team collaboration and maintenance. Consistency reflects attention to detail and conveys the high quality of the codebase.
- Popularity: Code should adopt modern programming practices, such as using Unicode, error handling, defensive programming, and portability design. Use currently recommended runtime libraries, API functions, and project settings.
- Reliability: Code must comply with laws, regulations, privacy policies, and security standards. Avoid demonstrating invasive or low-quality programming practices, ensuring that installation and execution processes are reversible and do not permanently change the machine state.
- Security: Code should demonstrate secure programming practices, such as following the principle of least privilege, using secure versions of runtime library functions, and adopting project settings recommended by SDL (Security Development Lifecycle).
Reasonable application of programming practices, design patterns, and language features is the key to achieving the above code qualities.
Basic Code Formatting
Indentation
Rule: All code should use 4 spaces for indentation. Tabs are strictly prohibited.
Reason: Different editors display tabs with different numbers of spaces, which leads to messy code formatting and reduced readability.
Suggestion: Configure the Visual Studio text editor to enable the "Insert spaces for tabs" option.
Braces
Braces must occupy a line by themselves. The formatting of braces is a point of contention for many. Some are accustomed to Java's style where the left brace follows the parenthesis, but Microsoft officially recommends that braces should occupy a line alone and not be on the same line as any statement.
The left brace "{" is placed on the next line of the keyword or method name and aligned with it. The left brace "{" should be aligned with the corresponding right brace "}".
Incorrect example:
public static void Main(string[] args) { }
Correct Example:
public static void Main(string[] args)
{
}
Special Requirement: if, while, do statements must use {}, even if {} is empty or contains only one statement.
Example:
if (somevalue == 1)
{
somevalue = 2;
}
Suggestion: It is recommended to add comments after the right brace } to facilitate finding the corresponding left brace.
Example:
{
while(1)
{
if(valid)
{
} // if valid
else
{
} // not valid
} // end forever
}
Exception: Auto-properties in classes can have braces on the same line.
Example:
public string Name { get; set; }
Using Sorting
Imported namespaces should be sorted alphabetically. The purpose of this is to facilitate quickly finding a namespace among multiple imported namespaces.
Line Breaks
When an expression exceeds or is about to exceed the display range of one line on the monitor, follow the rules below for line breaks:
- Break line after a comma.
- Break line before an operator.
Rule 1 takes precedence over Rule 2.
When the above rules lead to messy code, adopt more flexible line break rules yourself.
Empty Lines
Purpose: Empty lines are used to separate logically related code blocks, improving code readability.
Use two empty lines when:
- Interface and class are defined in the same file, between the interface and class definitions
- Enum and class are defined in the same file, between the enum and class definitions
- Multiple classes are defined in the same file, between class definitions
Use one empty line when:
- Between methods and methods, properties and properties
- Between variable declarations and statements in a method
- Between different logic blocks in a method
- Between return statements and other statements in a method
- Between properties and methods, properties and fields, methods and fields
- After statement control blocks, such as if, for, while, switch
- Comments and the statements they annotate do not break lines, but separate from other statements by one empty line
Spaces
Rule: Spaces should be used in the following situations:
-
Use a space between keywords and the left parenthesis
(. E.g.,while (true)Note: Do not use spaces between method names and the left parenthesis
(, which helps distinguish method calls from keywords in the code. -
When multiple parameters are separated by commas, add a space after each comma
-
Except for
., all binary operators should be separated from their operands by spaces- Unary operators,
++and--do not need spaces between them and their operands
- Unary operators,
-
Expressions in statements are separated by spaces. E.g.,
for (expr1; expr2; expr3)
Example:
a += c + d;
a = (a + b) / (c * d);
while (d++ == s++)
{
n++;
}
PrintSize("size is " + size + "\n");
File Definition
Rule:
- Generally, a .cs file can only define one class, interface, enum, or struct
- Special cases allow defining multiple classes in the same .cs file, such as code generated by code generators or two closely related classes
- The class name should be consistent with the .cs file name to facilitate finding the class name via the file name
Example: The UserInfo class should be in the UserInfo.cs file.
Statements
Rule: Do not place more than one code statement on the same line.
Reason: Multi-line statements make single-step debugging in the debugger more difficult.
Incorrect Example:
a = 1; b = 2;
Correct Example:
a = 1;
b = 2;
Naming Specifications
Basic Naming Specifications
Core Principle: Choose meaningful names for various types, functions, variables, attributes, and data structures so that they directly reflect their purpose. Self-documenting code is good code.
Specific Specifications:
- Names should describe "what" rather than "how", avoid using names that expose basic implementation, and retain the abstraction layer that simplifies complexity
- Example: Use
GetNextStudent()instead ofGetNextArrayElement()
- Example: Use
- Do not use uncommon or ambiguous shortened or abbreviated forms in identifier names
- Example: Use
GetTemperatureinstead ofGetTemp(Temp could be an abbreviation for Temperature or Temporary)
- Example: Use
- Abbreviations can be used for public types or commonly known abbreviations
- Example: Thread procedure (ThreadProc), Window procedure (WndProc), Dialog procedure function (DialogProc)
- Do not use underscores, hyphens, or any other non-alphanumeric characters
- Do not use abbreviations not universally accepted in the computer field
- When appropriate, use well-known abbreviations to replace lengthy phrase names
- Example: UI (User Interface), OLAP (On-line Analytical Processing)
- When using abbreviations, follow these rules:
- Use PascalCase or camelCase for abbreviations longer than two characters (e.g., HtmlButton or HTMLButton)
- Abbreviations of only two characters should be capitalized (e.g., System.IO, not System.Io)
Naming Principles Summary:
- Difficulty in choosing the correct name may indicate a need for further analysis or definition of the item's purpose
- Make names long enough to be meaningful, and short enough to avoid verbosity
- Unique names are programmatically used only to distinguish items
- Expressive names are to help people read; providing understandable names is meaningful
- Ensure chosen names comply with the rules and standards of the applicable language
Recommended Naming Conventions
PascalCase
- Definition: Capitalize the first letter of the identifier and the first letter of each subsequent concatenated word
- Scope: Can be used for identifiers of three or more characters
- Example:
BackColor,UserInfo,GetData
camelCase
- Definition: The first letter of the identifier is lowercase, and the first letter of each subsequent concatenated word is capitalized
- Example:
backColor,userInfo,getData
Not Recommended Naming Conventions
Hungarian Notation
- Definition: Hungarian notation is a naming convention invented by a Hungarian programmer. The basic principle is: variable name = attribute + type + object description
- Example:
m_bFlag(m indicates member variable, b indicates boolean, combined: "a member variable of a class, boolean type, is a status flag") - Requirement: Strictly prohibit the use of Hungarian notation (do not include type indicators in variable names)
Common Naming Specifications
1. Language Specifications
- Rule: Strictly prohibit mixing Pinyin and English naming, and strictly forbid using Chinese directly
- Reason: Correct English spelling and grammar make it easy for readers to understand and avoid ambiguity
- Note: Even pure Pinyin naming should be avoided
- Positive Example:
name,order,baidu,alibabaand other internationally common names can be regarded as English - Negative Example:
zhekou(discount),Shuliang(quantity),int 变量=1
2. Class Name Specifications
- Rule: Class names use PascalCase
- Exception: DTO/UID and other module function abbreviations or interface definitions (such as IInterface)
- Positive Example:
UserDTO,XmlService,TFlowInfo,TTouchInfo,IUserService - Negative Example:
userDto,XMLService,tflowInfo,ttouchInfo
3. Method and Variable Naming Specifications
- Rule: Method names, parameter names, member variables, and local variables all use camelCase (Note: In C#, method names typically use PascalCase. The original text says camelCase for method names, which contradicts standard C# conventions. However, I will translate faithfully to the source text, but standard C# is Pascal for methods).
- Positive Example:
name,getUserInfo(),userId
4. Constant Naming Specifications
- Rule: Constant naming uses PascalCase
- Suggestion: Words should strive for complete semantic expression; do not worry about long names
- Positive Example:
MaxStockCount - Negative Example:
Max_Count
5. Special Class Naming Specifications
- Abstract Class: Recommend ending with Base
- Exception Class: End with Exception
- Test Class: Start with the name of the class it tests and end with Test
- Requirement: Eliminate non-standard abbreviations, avoid ambiguity
- Negative Example: Abbreviating
NotFoundExceptiontoNotFoundEx
6. Custom Element Naming Principles
- Purpose: To achieve self-documenting code
- Requirement: When naming any custom programming element, try to use complete word combinations to express its meaning
- Negative Example: Arbitrary naming like
int a
7. Design Pattern Naming Specifications
- Requirement: If modules, interfaces, classes, or methods use design patterns, the specific pattern needs to be reflected in the naming
- Purpose: Reflecting design patterns in names helps readers quickly understand architectural design concepts
- Positive Example:
public class OrderFactory,public class LoginProxy
8. Interface and Implementation Class Naming Specifications
- Rule 1: For Service and DAO classes, the exposed service must be an interface
- Positive Example:
CacheServiceimplementsICacheServiceinterface
- Positive Example:
- Rule 2: If the interface name describes a capability, use the corresponding adjective as the interface name (usually ending in -able)
- Positive Example:
IDisposeableinterface
- Positive Example:
9. Enum Class Naming Specifications
- Rule: Enum class member names use PascalCase
- Description: Enums are special classes, and members are all constants
- Suggestion:
- Explicitly specify enum values for enum members to prevent confusion of enum values caused by inserting enum variables in the middle in the future
- It is best to apply the Description tag to each enum value
- Example:
public enum PaymentStatus : sbyte
{
/// <summary>
/// In Progress
/// </summary>
[Description("In Progress")]Processing = 1,
/// <summary>
/// Success
/// </summary>
[Description("Success")]Succeed = 2
}
Layer Naming Conventions
A) Service/DAO Layer Method Naming Conventions
- Get single object: Method uses
Getas prefix - Get multiple objects: Method uses
Listas suffix, e.g.,GetOrdersList - Get count: Method uses
Countas suffix - Add or Update: Method uses
SaveorAdd - Delete: Method uses
RemoveorDelete - Modify: Method uses
Update
B) Domain Model Naming Conventions
- Entity Object: e.g.,
UserInfo, entity name is the database table name - Data Transfer Object:
xxxDTO, xxx is the name related to the business domain - View Object:
xxxViewModel, xxx is generally the webpage name
Namespace and Assembly Naming
- Naming Rule: Namespace names use PascalCase, and the first character is capitalized
- Naming Format: Namespace names should reflect the overall function provided by their content as much as possible, generally using the "Domain.ProjectName.ModuleName" naming method
- Example:
Masuit.MyBlogs.Models
- Example:
- Folder Structure Consistency: Namespaces should be consistent with the folder hierarchy
- Example: In the project
Masuit.MyBlogs.Core, theMasuit.MyBlogs.Core.Infrastructure.Servicesnamespace corresponds to theMasuit.MyBlogs.Core/Infrastructure/Servicesfolder of the project
- Example: In the project
Common Identifier Naming Specifications
| Identifier | Specification | Naming Structure | Example |
|---|---|---|---|
| Class, Struct | PascalCase | Noun | public class ComplexNumber {...}public struct ComplextStruct {...} |
| Namespace | PascalCase Do not use the same name for a namespace and types within it. | Noun | namespace Microsoft.Sample.Windows7 |
| Enum | PascalCase Use plural nouns or noun phrases for flags enums, and singular nouns or noun phrases for simple enums. | Noun | [Flags]public enum ConsoleModifiers { Alt, Control } |
| Method | PascalCase | Verb or Verb Phrase | public void Print() {...}public void ProcessItem() {...} |
| Public Property | PascalCase Use the plural form of items in the collection, or singular noun followed by "List" or "Collection". Use affirmative phrases for boolean properties (CanSeek, not CantSeek). Can also use "Is", "Can" or "Has" as prefix if meaningful. | Noun or Adjective | public string CustomerNamepublic ItemCollection Itemspublic bool CanRead |
| Non-Public Property | camelCase Must use '_' prefix to maintain code consistency. | Noun or Adjective | private string _name; |
| Event | PascalCase Use present or past tense to indicate concept before or after event. Do not use "Before" or "After" prefix or suffix. | Verb or Verb Phrase | // Event raised after window closes.public event WindowClosed// Event raised before window closes.public event WindowClosing |
| Delegate | PascalCase Add 'EventHandler' suffix for delegates used in events. Add 'Callback' suffix for delegates other than event handlers. Do not add "Delegate" suffix. | public delegate WindowClosedEventHandler | |
| Interface | PascalCase, with 'I' prefix | Noun | public interface IDictionary |
| Constant | PascalCase for Public constants; camelCase for Internal constants; Abbreviations of only 1 or 2 characters must be all uppercase. | Noun | public const string MessageText = "A";private const string messageText = "B";public const double PI = 3.14159...; |
| Parameter, Variable | camelCase | Noun | int customerID; |
| Generic Parameter | PascalCase, with 'T' prefix Use descriptive names unless single character is sufficient. Prefix descriptive type parameters with T. Should use T for single character type parameter names. | Noun | T,TItem, TPolicy |
| Resource | PascalCase Provide strong descriptive identifiers. Keep concise but don't sacrifice readability for space. Use alphanumeric characters and underscores. | Noun | ArgumentExceptionInvalidName |
Variable Usage
Global Variables
- Principle: Use global variables as little as possible
- Usage Specification: To use global variables correctly, generally pass them as parameters into functions
- Precautions:
- Never reference global variables directly inside functions or classes, as this causes side effects: changing the state of global variables without the caller's knowledge
- This principle also applies to static variables
- Modification Specification: If a global variable needs to be modified, pass it as an output parameter or return a copy of the global variable
Variable Declaration and Initialization
-
Scope Principle: Declare local variables in the smallest scope block that contains them
-
Declaration Timing:
- If language permits, declare them only before use
- Otherwise declare at the top of the scope block
-
Correct Example:
void MyMethod()
{
int int1 = 0;
if (condition)
{
int int2 = 0;
...
}
}
-
Avoid Variable Name Conflicts: Avoid using the same variable name across different levels of scope
int count;...void MyMethod(){if (condition){int count = 0; // Avoid: Name conflict with outer scope variable...}...} -
Initialize Default Values: Always initialize variables with default values when declaring them to reduce the probability of null reference exceptions
-
Merge Declaration and Initialization: Where language permits, place local variable declaration and initialization or assignment on the same line
- Benefit: Reduces vertical space and ensures variables are not in an uninitialized state
-
Incorrect Example:
string str;
str="123";
- Correct Example:
string str="123";
- Single Line Declaration Rule: Only one declaration per line is recommended, sorted alphabetically
int level; // Recommendedint size; // Recommendedint x, y; // Not Recommended
Constant Definition
-
Prohibit Hard Coding: No undefined constants should appear directly in the code
- Negative Example:
string name = "tflow" + userId;
- Negative Example:
-
Numeric Type Suffix: Use uppercase L or F when assigning long or float values
- Note: L cannot be written as lowercase l, as lowercase l is easily confused with the number 1, causing misunderstanding
-
Constant Classification Maintenance: Do not use a single constant class to maintain all constants; classify them by function and maintain separately
- Reason: A large and all-encompassing constant class is disorganized, requiring search to locate constants, which is unfavorable for understanding and maintenance
- Positive Example: Cache-related constants in
CacheConstantsclass; system configuration constants inConfigConstants
-
Use Enum Types: If a variable value only changes within a fixed range, use
enumtype to define it -
Constant Definition Principle: Must define values that will never change as constant fields
- Compiler Behavior: The compiler embeds constant fields directly at the calling code
- Advantage: Constant values will never change and will not break compatibility
-
Example:
public class Int32
{
public const int MaxValue = 0x7fffffff;
public const int MinValue = unchecked((int)0x80000000);
}
- Predefined Object Instances: Must use
public static readonlyfields for predefined object instances- If there are predefined type instances, also define the type as
public static readonly - Example:
- If there are predefined type instances, also define the type as
public class ShellFolder
{
...
}
public static readonly ShellFolder ProgramData = new ShellFolder("ProgramData");
public static readonly ShellFolder ProgramFiles = new ShellFolder("ProgramData");
Distinction Between readonly and const Usage
-
Reason for using const: The main reason for using const is efficiency
- Efficiency Principle: After compilation, places referencing const variables are replaced with the actual values corresponding to the const variables
- Example:
const int MAX = 100;, MAX and 100 are equivalent in use - Feature: const inherently has static characteristics
-
Essential Difference:
constis a compile-time constant,readonlyis a runtime constantconstcan only modify primitive types, enum types, or strings;readonlyhas no restrictions
-
readonly Special Usage: Within a constructor, readonly fields can be assigned multiple times (only during initialization)
Comment Specifications
Documentation Comment Requirements
- Mandatory Use: All classes, interfaces, methods, and properties must use VS-supported documentation comments
- Format Specification: Use .Net defined Xml tags, used when declaring interfaces, classes, methods, properties, and fields
- Generation Method: Type
///above the method name or class name to generate a documentation comment block
/// <summary>
/// Comment
/// </summary>
/// <returns></returns>
Specific Element Comment Specifications
- Abstract Method Comments: All abstract methods (including methods in interfaces) must use VS comments. In addition to return values, parameters, and exception descriptions, must also point out what the method does and what function it implements
- Enum Comments: All enum type fields must have comments explaining the purpose of each enum value
Method Internal Comments
- Single Line Comments: Start a new line above the commented statement, try to use
//comments - Avoid Use: Avoid using
/* */comments - Format Requirement: Pay attention to alignment with code
Comment Language and Updates
- Language Selection: If the team does not require a specific language for comments, use localized language for code comments
- Synchronous Update: When code is modified, comments must be modified accordingly, especially modifications to parameters, return values, exceptions, core logic, etc.
Code Deletion and Comment Management
- Cautious Deletion: Be cautious about directly deleting code. Explain in detail above, rather than simply commenting it out. If useless, delete it
- Meaning of Code Comments: Code being commented out has two possibilities:
- This code logic will be restored later
- Permanently unused. If the former lacks remarks, it is difficult to know the motivation for commenting. The latter is recommended to be deleted directly (because the code repository saves historical code for rollback)
Comment Style Requirements
- Avoid Clutter: Avoid messy comments, such as a whole line of asterisks as a divider. Instead, use whitespace to separate comments from code
- Avoid Boxes: Avoid putting print boxes around block comments. It may look pretty, but it is hard to maintain
Pre-Release Processing
- Clean Temporary Comments: Before deployment and release, remove all temporary or irrelevant comments to avoid confusion during future maintenance work
Code Simplification Principles
- Refactor First: If comments are needed to explain complex code sections, check the code to determine if it should be rewritten. Do everything possible not to comment code that is hard to understand, but rewrite it instead
- Balance Performance and Maintainability: Although generally one should not sacrifice performance to make code simpler for people to use, a balance between performance and maintainability must be kept
Comment Timeliness
- Comment Promptly: Comment while writing code, because there may not be time later. Also, if there is a chance to review written code, what seems obvious today may not be obvious six weeks later
Comment Content Requirements
- Avoid Redundancy: Avoid redundant or inappropriate comments, should not contain personal emotional content, such as unnecessary humorous remarks (although the blogger often does this in projects)
- Complete Sentences: Use complete sentences when writing comments. Comments should clarify the code, not increase ambiguity
Mandatory Comment Requirements
- Must Comment Hard-to-Understand Code: Code that is hard to understand must be commented!!
Unified Comment Specifications
- Unified Style: Throughout the application, use a unified style with consistent punctuation and structure to construct comments
- Whitespace Separation: Use whitespace to separate comments from comment delimiters. When viewing comments with other text editors without an IDE, this makes comments obvious and easy to find
Version Control Principles
- Version Management: Code modification change records should not use comments to indicate modification date and modifier. Comments should only address code, not record versions. Code versions should be managed using a code versioning system
Core Comment Requirements
Comments should satisfy the following core requirements:
- Reflect Design Thought: Accurately reflect design thought and code logic
- Describe Business Meaning: Describe business meaning so that other developers can quickly understand the information behind the code
Meaning of Comments
- Self-Documentation: Comments are for oneself to see; even after a long time, one can clearly understand the train of thought at that time
- Knowledge Inheritance: Comments are also for successors, enabling them to quickly take over one's work
Balance of Comments
- Self-Commenting First: Good naming and code structure are self-commenting; comments strive to be concise, accurate, and expressive
- Avoid Extremes: Avoid the extreme of excessive comments. Once code logic is modified, modifying comments is a significant burden
- Focus on Intent: Comments are used to explain the design intent of a piece of code. Must not let comments be just some useless code
TODO Comments
TODO comments are a special comment type supported by Visual Studio, with the following characteristics:
- Automatic Recognition: When a comment is prefixed with
TODO, VS automatically perceives unfinished tasks at that comment - Task List Integration: Automatically adds TODO items to VS's task list, facilitating developers to quickly locate and manage pending items
Example
// TODO: Perfect error handling logic
public void ProcessData(List<string> data)
{
// Temporary implementation
foreach(var item in data)
{
Console.WriteLine(item);
}
}
Class and Interface Declaration
Basic Format Specifications
- Method Name and Parenthesis: No space between method name and its following left parenthesis
- Left Brace Position: The left brace
{appears on the next line of the declaration and aligns with it, occupying a line alone - Method Interval: Separate methods with one empty line
Class Declaration Specifications
Naming Rules
- Naming Method: Use PascalCase
- Naming Way: Name classes with nouns or noun phrases
- Abbreviation Rules: Use full names to avoid abbreviations, unless the abbreviation is a recognized convention, such as URL, HTML
- Prefix Requirement: Do not use type prefixes, such as using C prefix on class names. For example, use class name FileStream, not CFileStream
- Underscore Usage: Do not use underscore character
_
Special Case Handling
- Class Names Starting with I: Sometimes it is necessary to provide class names starting with the letter I, even though the class is not an interface. As long as I is the first letter of the entire word that forms part of the class name, this is appropriate. For example, class name IdentityStore is appropriate
Derived Class Naming
- Composite Naming: Where appropriate, use composite words to name derived classes
- Base Class Inclusion: The second part of the derived class name should be the name of the base class. For example, ApplicationException is an appropriate name for a class derived from a class named Exception, because ApplicationException is an Exception
- Reasonable Judgment: Please use reasonable judgment when applying this rule. For example, Button is an appropriate name for a class derived from Control. Although a button is a control, making Control part of the class name would unnecessarily lengthen the name
Class Declaration Example
public class FileStream
public class Button
public class String
Constructors
Constructor Work Content
- Minimize Workload: Minimize the workload of constructors. Apart from capturing constructor parameters and setting main data members, constructors should not have too much workload
- Lazy Initialization: Remaining workload should be deferred until necessary
Exception Handling
- Appropriately Throw Exceptions: Throw exceptions from within instance constructors when appropriate
Default Constructor
- Explicit Declaration: Explicitly declare the default constructor where needed. Even though the compiler sometimes automatically adds a default constructor for your class, explicit declaration makes the code easier to maintain
- Ensure Availability: This ensures that even if you add a constructor with parameters, the default constructor will still be defined
Virtual Method Call Restriction
- Prohibit Calling Virtual Methods: Never call virtual methods inside object constructors
- Risk Explanation: When calling a virtual method, the override method at the bottom of the inheritance hierarchy is actually called, without considering whether the constructor of the class defining that method has been called
Field Declaration
Access Modifier Specifications
- Prohibit Public Fields: Do not use public or protected instance fields
- Version Control Advantage: Avoiding exposing fields directly to developers makes it easier to version control the class, because fields cannot be changed to properties while maintaining binary compatibility
- Property Accessor Substitution: Consider providing get and set property accessors for fields instead of making them public
- Flexibility Improvement: The existence of executable code in get and set property accessors allows for subsequent improvements, such as creating objects as needed when using properties or receiving property change notifications
Private Field Example
public class Control : Component
{
private int _handle;
public int Handle
{
get
{
return _handle;
}
}
}
Field Naming Specifications
- Private/Protected Fields: private, protected use camelCase
- Public Fields: public use PascalCase
- Abbreviation Usage: Spell out all words used in field names. Use abbreviations only when developers generally understand them
class SampleClass
{
private string _url;
private string _destinationUrl;
}
Hungarian Notation Prohibition
- Prohibit Use: Do not use Hungarian notation for field names
- Naming Principle: Good names describe semantics, not types
Predefined Object Instances
- Use Public Static Readonly Fields: Use public static readonly fields for predefined object instances
- Declaration Position: If there are predefined instances of the object, declare them as public static readonly fields of the object itself
- Naming Specification: Use PascalCase because the fields are public
public struct Color
{
public static readonly Color Red = new Color(0x0000FF);
public Color(int rgb)
{
// Insert code here.
}
public Color(byte r, byte g, byte b)
{
// Insert code here.
}
public byte RedValue
{
get
{
// Correctly return red value
return _red; // Assuming _red is a private field
}
}
private byte _red; // Private field example
}
Static Fields
Naming Specifications
- Naming Way: Use nouns, noun phrases, or abbreviations of nouns to name static fields
- Case Rule: Use PascalCase
Usage Suggestion
- Prioritize Properties: Recommend using static properties instead of public static fields whenever possible
Parameter Declaration
Parameter Naming Specifications
- Use Descriptive Names: Use descriptive parameter names. Parameter names should be sufficiently descriptive so that the parameter's name and its type can be used to determine its meaning in most cases
- camelCase: Use camelCase for parameter names
- Describe Meaning Not Type: Use names that describe the parameter's meaning, not the parameter's type. Development tools will provide meaningful information about the parameter's type. Therefore, by describing meaning, parameter names can be better used
Parameter Naming Restrictions
- Prohibit Hungarian Notation: Do not add Hungarian type notation prefixes to parameter names
- Avoid Reserved Parameters: Do not use reserved parameters. Reserved parameters are private parameters; if needed, they can be exposed in future versions. Conversely, if more data is needed in a future version of the class library, please add a new overload for the method
Example
Type GetType(string typeName)
string Format(string format, object args)
Method Declaration
Method Naming Specifications
- Use Verb or Verb Phrase: Use verbs or verb phrases to name methods
- PascalCase: Use PascalCase
RemoveAll()
GetCharArray()
Invoke()
Situations Requiring Parameter Validation
- Low Call Frequency: Methods with low call frequency
- High Execution Overhead: Methods with large execution time overhead. In this case, parameter validation time is almost negligible, but if parameter errors cause intermediate execution rollback or errors, the loss outweighs the gain
- High Stability Requirement: Methods requiring extremely high stability and availability
- Open Interface: Open interfaces provided externally, whether RPC/API/HTTP interfaces
- Sensitive Permission Entry: Sensitive permission entries
Situations Not Requiring Parameter Validation
- Loop Call Method: Methods highly likely to be called in loops. But external parameter check requirements must be noted in the method description
- Low-Level High-Frequency Method: Methods with high low-level call frequency. After all, like the last step of purified water filtration, parameter errors are unlikely to be exposed only at the bottom layer. Generally, DAO layer and Service layer are in the same application, deployed on the same server, so DAO parameter validation can be omitted
- Internal Private Method: Methods declared as private and only called by own code. If it can be confirmed that the code calling the method passes parameters that have been checked or are definitely problem-free, parameter validation can be omitted
Asynchronous Methods
Naming and Identification Specifications
- Naming Convention: Asynchronous methods generally end with Async
- Method Signature: Method signature carries async identifier
- await Usage: await keyword appears inside the method body
Return Value Types
Asynchronous methods have three types of return values:
- void: No return value
- Task: Returns a Task, allowing the execution status of the asynchronous method to be obtained
- Task<T>: Returns
Task<T>, allowing the execution result and status of the asynchronous method to be obtained
Usage Suggestions
- Avoid void Return: If you think your asynchronous task does not need to know its execution status (whether an exception occurred, etc.), you can use void signature without return value (strongly recommended not to use void asynchronous methods in formal projects)
Example: Asynchronous Method Using void Return
public static async void FireAndForgetAsync()
{
var myTask = Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("DoWork : {0}", i);
Thread.Sleep(500);
}
});
await myTask;
}
Example: Asynchronous Method Using Task Return
If you need to know the execution status of the task, use Task signature
static Task SayHello(string name)
{
return Task.Run(() =>
{
Console.WriteLine("Hello: {0}", name);
});
}
static async Task SayHelloAsync(string name)
{
await SayHello(name);
}
Example: Asynchronous Method Using Task<T> Return
If you need to obtain the result of the asynchronous method, you can use Task<T> signature
static Task<int> SumArray(int[] arr)
{
return Task.Run(() => arr.Sum());
}
static async Task<int> GetSumAsync(int[] arr)
{
int result = await SumArray(arr);
return result;
}
static async void GetTaskOfTResult()
{
int[] arr = Enumerable.Range(1, 100).ToArray();
Console.WriteLine("result={0}", GetSumAsync(arr).Result);
int result = await GetSumAsync(arr);
Console.WriteLine("result={0}", result);
}
Asynchronous Lambda Expressions
Besides the three asynchronous method examples above, lambda expressions can also be used to create asynchronous methods. Just add async before the lambda expression parameters and use await inside the expression
public static void TestAsyncLambda()
{
Action act = async () =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Do Work {0}", i);
await Task.Delay(500);
}
};
act();
}
Property Declaration
Property Naming Specifications
- Use Noun or Noun Phrase: Use nouns or noun phrases to name properties
- PascalCase: Use PascalCase
- Type and Name Matching: Consider creating properties with the same name as the property's underlying type. For example, if declaring a property named Color, the type of the property should also be Color
Example: Basic Property Declaration
public class SampleClass{
public Color BackColor{get;set;}
}
Example: Property with Same Name and Type
The following code example illustrates providing a property whose name is the same as its type.
public enum Color
{
Red,
Green
}
public class Control
{
public Color Color { get; set; }
}
Event Declaration
Event Handler Naming
- Suffix Specification: Use EventHandler suffix for event handler names
Event Delegate Parameter Specifications
- Parameter Quantity and Name: Specify two parameters named sender and e
- sender Parameter Type: The sender parameter represents the object that raised the event and is always of object type, even when a more specific type could be used
- e Parameter Specification: State associated with the event is encapsulated in an instance of an event class named e. Use an appropriate and specific event class for the e parameter type
Event Argument Class Naming
- Suffix Specification: Name event argument classes with EventArgs suffix
Event Naming Specifications
- Verb Naming: Consider naming events with verbs
- Tense Usage: Use gerunds ("ing" form of verbs) to create event names representing concepts before the event, and past tense to represent after the event
- Example: A cancelable Close event should have a Closing event and a Closed event
- Avoid Use: Do not use BeforeXxx/AfterXxx naming patterns
- Avoid Prefix/Suffix: Do not use prefixes or suffixes on event declarations of types. For example, use Close, not OnClose
Event Override Support
- OnXxx Method: Typically, for events that can be overridden in derived classes, a protected method (called OnXxx) should be provided on the type
- Parameter Simplification: This method should only have the event parameter e, because the sender is always an instance of the type
public delegate void MouseEventHandler(object sender, MouseEventArgs e);
public class MouseEventArgs : EventArgs
{
int x;
int y;
public MouseEventArgs(int x, int y)
{
this.x = x;
this.y = y;
}
public int X
{
get
{
return x;
}
}
public int Y
{
get
{
return y;
}
}
}
Collections
Collection Definition
A collection is a group of similar typed objects combined together, such as hash tables, queries, stacks, dictionaries, and lists
Collection Naming Suggestions
- Use Plural Forms: Collection naming suggests using plural forms
Member Method Overloading
Overloading Priority Principle
- Use Method Overloading: Use member method overloading instead of defining member methods with default parameters
- Default Parameter Disadvantage: Default parameters are not CLS-compliant, so they cannot be reused by some languages
Default Parameter Versioning Issue
- Version Inconsistency Risk: Member methods with default parameters have a versioning issue
- Example Scenario:
- Version 1 of member method sets optional parameter default to 123
- When compiling code calling this method without specifying optional parameters, the compiler embeds 123 directly into the code at the call site
- When version 2 changes the default parameter to 863, if the calling code is not recompiled, it will call version 2 method but pass 123 as its parameter
Parameter Naming Specifications
- Maintain Parameter Name Consistency: Must not arbitrarily change parameter names in overloaded methods
- Same Name Parameter Position: If a parameter in one overloaded function represents the same parameter in another overloaded function, that parameter should have the same name and appear in the same position in the overloaded function
Virtual Function Setting Rules
- Virtual Function Restriction: Please only set the longest overloaded function as virtual (for extensibility consideration)
- Call Chain Requirement: Short overloaded functions should always call long overloaded functions
Virtual Member Methods
Performance Comparison
- Relative to Callbacks and Events: Virtual member methods perform better than callbacks and events
- Relative to Non-Virtual Methods: Virtual member methods perform slightly lower than non-virtual methods
Virtual Method Usage Principles
- Avoid Arbitrary Use: Do not set member methods as virtual without a reasonable reason
- Cost Awareness: You must be aware of the costs related to design, testing, and maintenance of virtual methods
Accessibility Suggestions
- Prioritize Protected: Should prefer setting virtual member methods to Protected accessibility rather than Public accessibility
- Extensibility Implementation: Public members should provide extensibility by calling Protected virtual methods (if needed)
Consider Implementing Comparators When Creating Objects
Comparator Implementation Principle
- Implement on Demand: Consider implementing comparators when there is a special need for comparison
Alternative Solutions
- LINQ Sorting: Collection sorting and comparison can also be solved via LINQ
Distinguish Between == and Equals
Basic Comparison Principles
- Value Type Comparison: For value types, if the values of the type are equal, return True
- Reference Type Comparison: For reference types, if the types point to the same object, return True
Comparability Overloadability
- Overload Possibility: Since operators "==" and "Equals" methods can be overloaded syntactically to represent "value equality" and "reference equality"
- Explicit Reference Comparison: To clarify that a method definitely compares "reference equality", FCL provides Object.ReferenceEquals method
- ReferenceEquals Function: This method compares whether two instances are the same instance
String Type Specificity
- Special Handling: For string, such a special reference type, Microsoft feels its practical meaning is closer to value types
- Comparison Behavior: In FCL, string comparison is overloaded for "value of type" comparison, not "reference itself" comparison
Override GetHashCode When Overriding Equals
Prerequisite for Overriding Equals
- Cautious Override: Unless considering the custom type will be used as a key in hash-based collections, it is not recommended to override Equals method
- Risk Warning: Overriding Equals method brings a series of problems
Collection Lookup Mechanism
- Lookup Process: When a collection finds a value, it essentially first looks up HashCode, and then finds that object to compare Equals
Type-Safe Interface Implementation
- Interface Requirement: While overriding Equals method, one should also implement a type-safe interface
IEquatable<T> - Example:
class Person : IEquatable<Person>
Output Formatted String for Type
Formatting Method Overview
There are two ways to provide formatted string output for a type
Active Implementation (IFormattable Interface)
- Implementation Requirement: Let type inherit interface IFormattable
- Applicable Scenario: When developers can foresee formatting requirements for the type
- Feature: This is an active implementation way for the type
Custom Formatter Way
- Usage Scenario: More often, users of the type need to customize formatters for the type
- Advantage: This is the most flexible way, allowing multiple formatters to be provided for the type as requirements change
Formatter Interface Requirement
- Necessary Interfaces: A typical formatter should inherit interfaces IFormatProvider and ICustomFormatter
Correctly Implement Shallow Copy and Deep Copy
Shallow Copy Definition and Characteristics
- Basic Concept: Copy all fields in the object to a new object (copy)
- Value Type Handling: After value type fields are copied to the copy, modifications in the copy do not affect the corresponding values in the source object
- Reference Type Handling: What is copied to the copy for reference type fields is the reference of the reference type, not the referenced object
- Modification Impact: Modifications to reference type field values in the copy will affect the source object itself
Deep Copy Definition and Characteristics
- Basic Concept: Also copy all fields in the object to a new object
- Handling Way: Whether object's value type fields or reference type fields, they will be recreated and assigned
- Modification Isolation: Modifications to the copy will not affect the source object itself
Clone Interface Implementation Suggestion
- Interface Recommendation: Microsoft recommends using the way of inheriting ICloneable interface to explicitly tell callers: this type can be copied
- Interface Feature: ICloneable interface only provides a method declared as Clone
- Implementation Flexibility: Shallow copy or deep copy can be implemented inside Clone method according to requirements
Example: Shallow Copy Implementation Code
class Employee:ICloneable
{
public string IDCode {get;set;}
public int Age {get;set; }
public Department Department{get;set;}
#region ICloneable Members
public object Clone()
{
return this.MemberwiseClone();
}
#endregion
}
class Department
{
public string Name {get;set;}
public override string ToString()
{
return this.Name;
}
}
String Type Specificity in Shallow Copy
Notice that Employee's IDCode property is string type. Theoretically string type is a reference type, but due to the specificity of this reference type (both implementation and semantics), Object.MemberwiseClone method still creates a copy for it. That is to say, in the shallow copy process, we should treat strings as value types.
Example: Deep Copy Implementation Sample
(Suggest using serialization form to perform deep copy)
class Employee:ICloneable
{
public string IDCode{get;set;}
public int Age{get;set;}
public Department Department{get;set;}
#region ICloneable Members
public object Clone()
{
using(Stream objectStream=new MemoryStream())
{
IFormatter formatter=new BinaryFormatter();
formatter.Serialize(objectStream,this);
objectStream.Seek(0,SeekOrigin.Begin);
return formatter.Deserialize(objectStream)as Employee;
}
}
#endregion
}
Simultaneously implement deep copy and shallow copy:
Since interface ICloneable only has an ambiguous Clone method, if one wants to implement both deep copy and shallow copy in a class, one can only implement two extra methods ourselves, declared as DeepClone and Shallow. The final version of Employee should look like the following form:
[Serializable]
class Employee:ICloneable
{
public string IDCode{get;set;}
public int Age{get;set;}
public Department Department{get;set;}
#region ICloneable Members
public object Clone()
{
return this.MemberwiseClone();
}
#endregion
public Employee DeepClone()
{
using(Stream objectStream=new MemoryStream())
{
IFormatter formatter=new BinaryFormatter();
formatter.Serialize(objectStream,this);
objectStream.Seek(0,SeekOrigin.Begin);
return formatter.Deserialize(objectStream)as Employee;
}
}
public Employee ShallowClone()
{
return Clone()as Employee;
}
}
Interface Declaration
Name interfaces with nouns or noun phrases, or adjectives describing behavior. Example:
Interface name IComponent uses descriptive noun Interface name ICustomAttributeProvider uses noun phrase Interface name IPersistable uses adjective.
Use PascalCase. Use few abbreviations. Prefix. Do not use underscore character "_". Add letter I prefix to interface name to indicate the type is an interface. Use similar names when defining class/interface pairs (where class is standard implementation of interface). The difference between the two names should just be the letter I on the interface name
public interface IServiceProvider
public interface IFormatable
The following code example illustrates how to define IComponent interface and its standard implementation Component class.
public interface IComponent{
// Implementation code goes here.
}
public class Component: IComponent {
// Implementation code goes here.
}
About Enums
Each value of an enum represents a specific meaning, it is a strongly typed constant parameter;
Must use enum to define enums, not constant classes. Enum type is a struct with a collection of static constants. If complying with these specifications, defining enum types instead of structs with static constants will get extra compiler and reflection support.
Incorrect Example:
public static class Color
{
public const int Red = 0;
public const int Green = 1;
public const int Blue = 2;
}
Correct Example:
public enum Color
{
Red,
Green,
Blue
}
Explicitly specify enum values to prevent confusion of enum values caused by inserting enum variables in the middle in the future.
Must not use Enum.IsDefined in .NET to check enum range. Enum.IsDefined has 2 problems. First, it loads reflection and a large amount of type metadata, which is extremely expensive. Second, it has versioning issues.
Correct Example
if (c > Color.Black || c < Color.White){
throw new ArgumentOutOfRangeException("...");
}
Incorrect Example:
if (!Enum.IsDefined(typeof(Color), c)){ throw new InvalidEnumArgumentException(...);}
Use 0 Value as Default for Enums
Allowed enum types are byte, sbyte, short, ushort, int, uint, long, and ulong. Should always use 0 value as default value for enum types. However, doing so is not because the allowed enum types default to 0 value when declared, but has engineering significance.
Since enum types start from 0, this avoids an extra 0 value appearing in a week.
Avoid Providing Explicit Values for Enum Type Elements
Do not set values for enums. Sometimes there are needs to add, elements will be added to enums. At this time, just like we add element ValueTemp to enum, it is very likely to accidentally add an invalid value.
Strings
Must not use '+' operator to concatenate large amounts of strings. We should use StringBuilder to implement concatenation work. However, when concatenating small amounts of strings, C# 6's $ interpolated strings are recommended.
Correct Example:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++){
sb.Append(i.ToString());
}
Must use overload functions that explicitly specify string comparison rules. Generally, need to call overload functions with StringComparison type parameter.
Must use StringComparison.Ordinal and StringComparison.OrdinalIgnoreCase as safe defaults when comparing culture-agnostic strings to improve performance.
Must use string operations based on StringComparison.CurrentCulture when outputting results to users.
Must use non-linguistic StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase values when comparing language-agnostic strings (symbols, etc.), instead of string operations based on CultureInfo.InvariantCulture. Generally speaking, do not use string operations based on StringComparison.InvariantCulture. An exception is when you insist it is linguistically meaningful but culturally agnostic.
Must use overloaded version of String.Equals to test if 2 strings are equal. For example, after ignoring case, check if 2 strings are equal,
if (str1.Equals(str2, StringComparison.OrdinalIgnoreCase))
Must not use overloaded versions of String.Compare or CompareTo to check if return value is 0 to judge if strings are equal. These 2 functions are for string sorting, not checking equality.
Must use String.ToUpperInvariant function to normalize strings during string comparison, not String.ToLowerInvariant.
Correctly Operate Strings
Concatenating strings must consider using StringBuilder, default length is 16, set according to actual situation.
StringBuilder essence: allocates memory in unmanaged way.
Also StringFormat method internally uses StringBuilder for string formatting.
Arrays and Collections
We should use arrays in low-level functions to reduce memory consumption and enhance performance. For public interfaces, prefer collections.
Collections provide more control over their content, can improve over time, and improve usability. In addition, using arrays in read-only scenarios is not recommended because the cost of array cloning is too high. However, using arrays for read-only scenarios is also a good idea. Arrays occupy relatively low memory, and can access array elements faster due to runtime optimization.
Must not use read-only array fields. The field itself is read-only and cannot be modified, but its internal elements can be modified. The following example shows the trap of using read-only array fields:
Incorrect Example:
public static readonly char[] InvalidPathChars = { '\"', '<', '>', '|'};
Caller can modify values inside the array:
InvalidPathChars[0] = 'A';
We can use a read-only collection (as long as its elements are also immutable), or perform array cloning before returning. However, the cost of array cloning may be too high:
public static ReadOnlyCollection<char> GetInvalidPathChars()
{
return Array.AsReadOnly(badChars);
}
public static char[] GetInvalidPathChars()
{
return (char[]) badChars.Clone();
}
We should use jagged arrays instead of multidimensional arrays. A jagged array refers to its elements themselves being an array. Arrays constituting elements may have different sizes, so compared to multidimensional arrays, it can reduce some dataset space waste (e.g., sparse matrices). In addition, CLR can optimize indexing operations on jagged arrays, so in some scenarios, it has better performance.
// Jagged array
int[][] jaggedArray =
{
new int[] {1, 2, 3, 4},
new int[] {5, 6, 7},
new int[] {8},
new int[] {9}
};
// Multidimensional array
int[,] multiDimArray =
{
{1, 2, 3, 4},
{5, 6, 7, 0},
{8, 0, 0, 0},
{9, 0, 0, 0}
};
Must declare properties or return values representing read/write collections as Collection<T> or its subclass, and declare properties or return values representing read-only collections as ReadOnlyCollection<T> or its subclass.
We should reconsider usage of ArrayList, because all objects added to it are treated as System.Object. When retrieving values from ArrayList, these objects will unbox and return their real value types. So we recommend you use custom typed collections instead of ArrayList. For example, .NET provides strongly typed collection StringCollection for String in System.Collection.Specialized namespace.
We should reconsider usage of Hashtable. Instead, you should try other dictionary classes, such as StringDictionary, NameValueCollection. Unless Hashtable only stores a small amount of values, it is best not to use Hashtable.
We should implement IEnumerable interface when implementing collection types, so that the collection can be used for LINQ to Objects.
Must not implement IEnumerator<T> and IEnumerable<T> interfaces on the same type simultaneously. Similarly, do not implement non-generic interfaces IEnumerator and IEnumerable simultaneously. So, a type can only be a collection or an enumerator, but not both.
Must not return null references for arrays or collections. The meaning of null arrays or collections is difficult to understand in code context. For example, a user might assume the following code works properly, so an empty array or collection should be returned instead of a null reference.
int[] arr = SomeOtherFunc();
foreach (int v in arr)
{
...
}
Regarding handling of HashCode and Equals, follow rules below:
- As long as Equals is overridden, GetHashCode must be overridden.
- Because HashSet stores unique objects, judging based on HashCode and Equals, so objects stored in HashSet must override these two methods.
Do not perform Remove/Add operations on elements inside foreach loop. Remove elements should use Linq way. If concurrent operation, need to lock List object.
List<string> list = new List<string>();
list.Add("1");
list.Add("2");
list.RemoveAll(o => o == "0");
Structs
Must ensure all instance data is set to 0 value, false, or null. When creating struct arrays, this prevents accidental creation of invalid instances.
Must implement IEquatable<T> interface for value types. Object.Equals method for value types causes boxing operations. And because it uses reflection features, its default implementation efficiency is not high. IEquatable<T>.Equals has considerable performance improvement over it, and its implementation can avoid causing boxing operations.
Struct vs Class
Must not define structs without special circumstances, unless it has following characteristics:
- It logically represents a single value, similar to primitive types (e.g., int, double, etc.).
- Its size is less than 16 bytes.
- It is immutable.
- It will not cause frequent boxing and unboxing operations.
In other cases, you should define classes instead of structs.
Static Classes
Must use static classes reasonably. Static classes should be used for core support helper classes based on objects within the framework.
Abstract Classes
Must not define Public or Protected-Internal constructors in abstract classes. Must define a Protected or Internal constructor for abstract classes. Protected constructors are more common because they allow the base class to complete its own initialization work when subclasses are created.
public abstract class Claim{
protected Claim()
{
...
}
}
internal constructors are used to restrict implementation of abstract classes to the assembly defining that class.
public abstract class Claim{
internal Claim()
{
...
}
}
Generic Delegates and Events
Always Prioritize Generics
The advantages of generics are multifaceted. Both generic classes and generic methods possess reusability, type safety, and high efficiency simultaneously, which non-generic classes and non-generic methods cannot possess.
Avoid Declaring Static Members in Generic Types
Actually, as you specify different data types for T, MyList<T> correspondingly becomes different data types, and static members are not shared between them.
However, if the data type specified by T is consistent, static members can still be shared between two generic objects, such as local List<string> and List<string> variables. But to avoid confusion caused by this, it is still recommended to try to avoid declaring static members of generic types in actual coding work.
Generic Methods in Non-Generic Types Do Not Generate Different Types in Runtime Native Code.
For example:
static void Main(string[]args)
{
Console.WriteLine(MyList.Func<int>());
Console.WriteLine(MyList.Func<int>());
Console.WriteLine(MyList.Func<string>());
}
class MyList
{
static int count;
public static int Func<T>()
{
return count++;
}
}
Output 0 ;1;2
Set Constraints for Generic Parameters
In coding process, should always consider setting constraints for generic parameters. Constraints make generic parameters a real "object", giving it the behaviors and properties we want, rather than just an object.
Specify constraint examples:
- Specify parameter is value type. (Except Nullable) where T:struct
- Specify parameter is reference type. where T:class
- Specify parameter has parameterless public constructor. where T:new()
- Note, CLR currently only supports parameterless constructor constraint.
- Specify parameter must be specified base class, or derived from specified base class.
- Specify parameter must be specified interface, or implement specified interface.
- Specify type parameter provided by T must be parameter provided for U, or derived from parameter provided for U. where T:U
Multiple constraints can be applied to parameters of the same type, and constraints themselves can be generic types.
Use default to Specify Initial Value for Generic Type Variables
Some algorithms, such as Find algorithm of generic collection List<T>, the object to find might be value type or reference type. Inside such algorithms, we often specify default values for these value type variables or reference type variables. So, the problem comes: default initial value for value type variable is 0, and default initial value for reference type variable is null. Obviously, this leads to compilation error in code below:
public T Func<T>()
{
T t=null;
T t=0;
return t;
}
Code "T t=null;" will warn in Visual Studio compiler: Error 1 Cannot convert Null to type parameter "T" because it could be a non-nullable value type. Consider using "default(T)". Code "T t=0;" will warn: Error 1 Cannot implicitly convert type "int" to "T".
Improvement
public T Func<T>()
{
T t=default(T);
return t;
}
Use Delegate Declarations in FCL
Note that there are three types of such delegate declarations in FCL, they are: Action, Func, Predicate. Especially after their generic versions came out, they can meet most needs in our actual coding process.
We should be accustomed to using such delegates in code to replace our own delegate declarations.
Besides Action, Func, and Predicate, there are delegate declarations in FCL used to represent special meanings.
// Delegate declaration used to represent event registration method:
public delegate void EventHandler(object sender,EventArgs e);
public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e);
// Delegate declaration representing thread method:
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object obj);
// Delegate declaration representing asynchronous callback:
public delegate void AsyncCallback(IAsyncResult ar);
Each type of delegate declaration in FCL represents a special type of usage. Although one can use one's own delegate declaration to replace it, doing so is not only unnecessary but also makes code lose conciseness and standardization. Before implementing our own delegate declaration, we should first check MSDN and do so only after confirming it is necessary.
Use Lambda Expressions Instead of Methods and Anonymous Methods
Skillfully use it in actual coding work to avoid writing cumbersome and unsightly code.
Beware of Traps in Closures
If an anonymous method (Lambda expression) references a local variable, the compiler will automatically promote that reference into the closure object, i.e., modifying variable i in for loop to reference public variable i of closure object (automatically created by compiler).
Example as follows:
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
for(int i=0;i<5;i++)
{
Action t=()=>
{
Console.WriteLine(i.ToString());
};
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
Above result all output 5;
Another implementation way;
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
TempClass tempClass=new TempClass();
for(tempClass.i=0;tempClass.i<5;tempClass.i++)
{
Action t=tempClass.TempFuc;
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
class TempClass
{
public int i;
public void TempFuc()
{
Console.WriteLine(i.ToString());
}
}
This code demonstrates closure object. So-called closure object refers to TempClass object in above situation (in first code, it is "<>c__DisplayClass2" object generated by compiler for us). If anonymous method (Lambda expression) references a local variable, compiler automatically promotes that reference to closure object, i.e., modifying variable i in for loop to reference public variable i of closure object. In this way, even if code execution leaves scope of original local variable i (like for loop), scope containing that closure object still exists. Understanding this helps understand code output.
Understand Essence of Delegates
Understanding delegates in C# requires grasping two key points:
- Delegate is method pointer.
- Delegate is a class. When instantiating it, reference method is passed as argument to its constructor.
Use event Keyword to Protect Delegates
First, without event protection, we can modify and assign to delegate at any time, to the extent that one method changes another method's delegate chain reference, e.g., assigning to null, causing exception when called in another method.
With event protection, when we modify, e.g.:
fl.FileUploaded=null;
fl.FileUploaded=Progress;
fl.FileUploaded(10);
Above code compilation will show error warning:
Event "ConsoleApplication1.FileUploader.FileUploaded" can only appear on left side of += or -= (except when used from type "ConsoleApplication1.FileUploader")
Implement Standard Event Model
With event protection above, it is still not standard enough.
EventHandler prototype declaration:
public delegate void EventHandler(object sender,EventArgs e);
Several specifications set by Microsoft for event model:
- Delegate type name ends with EventHandler;
- Delegate prototype return value is void;
- Delegate prototype has two parameters: sender represents event trigger, e represents event parameter;
- Event parameter name ends with EventArgs.
Use Generic Parameters to Compatible with Interface Immutability
Letting return value type return a type derived more than declared type is "covariance".
Compiler checks for interface and delegate type parameters are very strict. Unless specially declared with keyword out, this code will fail to compile. For example below
Example:
class Program{
static void Main(string[]args)
{
ISalary<Programmer>s=new BaseSalaryCounter<Programmer>();
PrintSalary(s);
}
static void PrintSalary(ISalary<Employee>s)
{
s.Pay();
}
}
interface ISalary<T>
{
void Pay();
}
class BaseSalaryCounter<T>:ISalary<T>
{
public void Pay()
{
Console.WriteLine("Pay base salary");
}
}
class Employee
{
public string Name{get;set;}
}
class Programmer:Employee{}
class Manager:Employee{}
Error: Cannot convert "ConsoleApplication4.ISalary<ConsoleApplication4.Programmer>" to "ConsoleApplication4.ISalary<ConsoleApplication4.Employee>"
To make PrintSalary fulfill requirement, we can use generic type parameter:
static void PrintSalary<T>(ISalary<T>s)
{
s.Pay();
}
Actually, as long as generic type parameter is not used as input parameter of method in an interface declaration, we can tentatively treat it as "return value" type. So, generic type parameter pattern satisfies definition of "covariance". However, as long as T is used as input parameter, it does not satisfy definition of "covariance". Like:
interface ISalary<out T>
{
void Pay(T t);
}
Compilation will prompt: Variance invalid: Type parameter "T" must be valid contravariantly on "ISalary.Pay(T)". "T" is covariant.
Make Generic Parameters in Interface Support Covariance
Besides using generic parameters to compatible with generic interface immutability mentioned in 11, another way is to add out keyword to generic declaration in interface to support covariance.
out keyword is new feature in FCL 4.0, it can be used in generic interfaces and delegates to make type parameters support covariance. Through covariance, one can use parameters derived more than declared parameters. We should be able to understand this application through example below.
For example:
static void Main(string[]args)
{
ISalary<Programmer>s=new BaseSalaryCounter<Programmer>();
ISalary<Manager>t=new BaseSalaryCounter<Manager>();
PrintSalary(s);
PrintSalary(t);
}
static void PrintSalary(ISalary<Employee>s)//Usage correct
{
s.Pay();
}
}
interface ISalary<out T> //Used out keyword
{
void Pay();
}
FCL 4.0 modified multiple interfaces to support covariance, such as IEnumerable<out T>, IEnumerator<out T>, IQuerable<out T>, etc. Since IEnumerable<out T> now supports covariance, above code runs well in FCL 4.0.
In our own code, if writing generic interface, unless confirming generic parameter in that interface involves no variance, it is recommended to add out keyword. Covariance increases usage scope of interface and brings almost no side effects.
Understand Covariance in Delegates
Generic variables in delegates naturally support covariance partially.
For example:
public delegate T GetEmployeeHanlder<T>(string name);
static void Main(){
GetEmployeeHanlder<Employee>getAEmployee=GetAManager;
Employee e=getAEmployee("Mike");
}
Because following situation exists, compilation fails:
GetEmployeeHanlder<Manager>getAManager=GetAManager;
GetEmployeeHanlder<Employee>getAEmployee=getAManager;
static Manager GetAManager(string name)
{
Console.WriteLine("I am Manager:"+name);
return new Manager(){Name=name};
}
static Employee GetAEmployee(string name)
{
Console.WriteLine("I am Employee:"+name);
return new Employee(){Name=name};
}
To make above code compile, also need to specify out keyword for generic parameter in delegate:
public delegate T GetEmployeeHanlder<out T>(string name);
Some delegate declarations in FCL 4.0 already use out keyword to make delegates support covariance, such as we often use:
public delegate TResult Func<out TResult>() and
public delegate TOutput Converter<in TInput,out TOutput>(TInput input)
Specify Contravariance for Generic Type Parameters
Contravariance means method parameter can be base class of delegate or generic interface parameter type. Common delegates supporting contravariance in FCL 4.0 are:
Func<in T, out TResult>
Predicate<in T>
Common generic interfaces are:
IComparer<in T>
Example:
class Program
{
static void Main()
{
Programmer p = new Programmer{Name="Mike"};
Manager m = new Manager{Name="Steve"};
Test(p, m);
}
static void Test<T>(IMyComparable<T> t1, T t2)
{
// Omitted
}
}
public interface IMyComparable<in T>
{
int Compare(T other);
}
public class Employee : IMyComparable<Employee>
{
public string Name { get; set; }
public int Compare(Employee other)
{
return Name.CompareTo(other.Name);
}
}
public class Programmer : Employee, IMyComparable<Programmer>
{
public int Compare(Programmer other)
{
return Name.CompareTo(other.Name);
}
}
public class Manager : Employee
{
}
In the example above, if in keyword is not specified for generic parameter T of interface IMyComparable, it will cause Test(p, m) compilation error. Due to introduction of interface contravariance, method Test supports more application scenarios. This should always be noted in actual coding of versions after FCL 4.0.
Collections and LINQ
Do Not Use Arrays When Element Quantity Is Variable
In C#, once array is created, length cannot change. If we need a dynamic and variable length collection, we should use ArrayList or List<T> to create. And array itself, especially one-dimensional array, when encountering algorithms requiring high efficiency, will be specially optimized to improve its efficiency. One-dimensional array is also called vector, its performance is best, using special instructions in IL to handle them (like newarr, ldelem, ldelem a, ldlen and stelem).
From memory usage perspective, array is allocated a fixed length of memory upon creation. If array elements are value types, length of each element equals length of corresponding value type; if array elements are reference types, length of each element is IntPtr.Size of that reference type. Once array storage structure is allocated, it cannot change. While ArrayList is array structure, can dynamically increase/decrease memory space, if ArrayList stores value types, it will add 12 bytes space for each element, where 4 bytes for object reference, 8 bytes for object header introduced when element is boxed. List<T> is generic implementation of ArrayList, saving overhead of unboxing and boxing.
Note: Due to array's memory characteristics, when using arrays, attention should also be paid to large object issues. So-called "large objects" refer to objects occupying more than 85,000 bytes of memory, allocated in Large Object Heap. Allocation and collection of large objects are different from small objects, especially collection, large object collection brings very low efficiency issues. So, cannot arbitrarily specify excessively large length for array, making array a large object.
If array length must be dynamically changed, one way is to convert array to ArrayList or List<T>, when expansion needed, internal array will automatically double in capacity.
Another way is using array copy function. Array inherits from System.Array, abstract class System.Array provides some useful implementation methods, including Copy method, responsible for copying content of one array to another array. Whichever method, changing array length is equivalent to recreating an array object.
Use foreach for Loop Traversal in Most Cases
Adopting foreach maximizes code simplification. It is used to traverse collection elements inheriting IEnumerable or IEnumerable<T> interface. With help of IL code, can see foreach essentially uses iterator to perform collection traversal. As follows:
List<object> list = new List<object>();
using (List<object>.Enumerator CS$5$0000 = list.GetEnumerator())
{
while (CS$5$0000.MoveNext())
{
object current = CS$5$0000.Current;
}
}
Besides code conciseness, foreach has two advantages:
- Automatically places code into try-finally block
- If type implements IDispose interface, it automatically calls Dispose method after loop ends.
foreach Cannot Replace for
One problem with foreach is: it does not support add/remove operations on collection during loop. Alternative method is to use for loop.
Unsupported reason: foreach loop uses iterator to traverse collection, it maintains a collection version control inside iterator provided by FCL. What is collection version? Simply put, it is an integer variable, any add/remove operation on collection will increment version number by 1. foreach loop calls MoveNext method to traverse elements, version number check is performed inside MoveNext method, once version number change detected, InvalidOperationException is thrown.
Using for loop does not bring such problem. for directly uses indexer, it does not judge collection version number, so there is no exception caused by collection change (of course, except index out of range situation).
public bool MoveNext()
{
List<T> list = this.list;
if ((this.version == list._version) && (this.index < list._size))
{
this.current = list._items[this.index];
this.index++;
return true;
}
return this.MoveNextRare();
}
Whether for loop or foreach loop, internally they access that array, and iterator just performs one more version check. In fact, inside loop, IL code generated by both is similar.
Use More Effective Object and Collection Initialization
Example:
class Program {
static void Main(string[] args)
{
Person person = new Person(){Name="Mike", Age=20};
}
}
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Object initializer supports assigning to auto-implemented properties in braces. Previously could only rely on constructor passing values, or assign to properties after object construction. Now these steps are simplified, initializer is actually equivalent to compiler assigning to properties after object generation.
Collection initialization is also simplified:
List<Person> personList = new List<Person>()
{
new Person() {Name="Rose", Age=19},
mike,
null
};
Key Point: Initializers are definitely not just for convenience of object and collection initialization, their more important role is to initialize properties for anonymous types in LINQ queries. Since properties of anonymous types returned by LINQ queries are read-only, if need to assign to anonymous type properties, or add properties, can only be done via initializers. Initializers can also use expressions for properties.
Example:
List<Person> personList2 = new List<Person>()
{
new Person(){Name="Rose", Age=19},
new Person(){Name="Steve", Age=45},
new Person(){Name="Jessica", Age=20}
};
var pTemp = from p in personList2
select new { p.Name, AgeScope = p.Age > 20 ? "Old" : "Young" };
foreach (var item in pTemp)
{
Console.WriteLine(string.Format("{0}:{1}", item.Name, item.AgeScope));
}
Use Generic Collections Instead of Non-Generic Collections
Note, non-generic collections are in System.Collections namespace, corresponding generic collections are in System.Collections.Generic namespace.
Benefits of generics are self-evident. If looping, casting, or unboxing and boxing operations are performed on large collections, impact of using traditional collections like ArrayList on efficiency is very large. In view of this, Microsoft provides support for generics. Generics use a pair of <> brackets to enclose actual type, then compiler and runtime complete remaining work.
Choose Correct Collection
To choose correct collection, first need to understand some data structure knowledge. So-called data structure is collection of data elements with one or more specific relationships between them.
Explanation: Advantage of direct storage structure is: adding elements to data structure is very efficient, just place in first empty slot at end of data. Disadvantage is: inserting elements into collection becomes inefficient, it needs to make room for inserted element and sequentially move subsequent elements.
If number of items in collection is fixed and no casting involved, using array is efficient, otherwise use List<T> (when array should be used, still use array)
Sequential storage structure, i.e., linear list. Linear list can dynamically expand and shrink, it stores data elements in a continuous area. Linear list cannot lookup by index, it searches elements by reference to address, to find an element, it must traverse all elements until finding corresponding element. So, advantage of linear list is high efficiency in inserting and deleting data, disadvantage is relatively lower lookup efficiency.
Queue Queue<T> follows FIFO pattern, it adds elements at end of collection, deletes elements at start of collection.