Solution vs Project
A solution is simply a collection of projects.
DLL
DLL stands for Dynamic Link Library. The executable file (exe file) uses the DLL file. So if we remove the DLL, the *.exe would no longer work. Any code created in C# will be compiled to a DLL file.
Variable Naming Rules
- C# keywords can’t be used as variable names
- Names can contain letters, digits and the underscore character, but the first character cannot be a digit.
- C# is case-sensitive, thus, the names count and Count refer to 2 different variables.
- The standard Microsoft-recommended convention is to use lower camel case when naming variables.
Comment
Single Line Comment
// todo: handle user's inputMultiple Lines Comment
/* This
* is
* multiple
* line
* comment */Parameter vs Argument
Parameters are names listed in the method definition, and arguments are the actual values passed to this method, so parameters are initilized to the values of the arguments provided.
Debugging
Turn on debugger之后,如果execution已经超过了我们的breakpoint该怎么办?visual studio有个办法就是拖动黄箭头回到breakpoint的地方,这样就能重新执行了。
Parsing
In general, a process of transforming a string into a different type is called parsing.
String Interpolation
string res = $"First is: {var1}, second is: {var2}";Switch Statement
switch(userChoice)
{
case "S":
case "s":
MethodA(...);
break;
case "A":
MethodB(...);
break;
default:
DefaultMethod(...);
}Switch Expression
char ConvertPointsToGrade(int points)
{
return points switch
{
10 or 9 => 'A',
8 or 7 or 6 => 'B',
5 or 4 or 3 => 'C',
_ => '!',
};
}Switch Expression and Pattern Matching
// Constant Pattern Mathcing: Matching specific values, similar to tradidtional case statements
string GetCaterogy(int number) => number switch
{
1 => "One",
2 => "Two",
3 => "Three",
_ => "Other"
};
// Relational Pattern Matching(>=, <=): Using rages instead of listing out every possible value
string getGrade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
// Type Pattern Matching: Handling different types dynamically
string DescribeObject(object obj) => obj switch
{
int n => $"It's an integer: {n}",
string s => $"It's a string: {s}",
bool b => $"It's a boolean: {b}",
_ => "Unknown type"
};
// Positional Pattern Matching: matching multiple values at once using tuples
string WeatherAdvice(string weather, bool isWeekend) => (weather, isWeekend) switch
{
("Sunny", true) => "Go to the beach!",
("Sunny", false) => "Enjoy a walk after work.",
("Rainy", _) => "Stay inside and read a book.",
_ => "Just another day."
};
// Property Pattern Mathcing (Matching Object Properties): Extracting and matching specific properties inside an object
string GetDiscount(Person person) => person switch
{
{ Age: < 12 } => "Child discount",
{ Age: >= 65 } => "Senior discount",
_ => "Regular price"
};
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}Foreach Loop
var words = new [] {"one", "two", "three", "four"};
foreach(var word in words)
{
Console.WriteLine(word);
}List
List<string> words = new List<String>();
words.Add("One");
Console.WriteLine("Count of elements is: " + words.Count);
var words2 = new List<String>
{
"one",
"two",
};
foreach(var word in words2)
{
Console.WriteLine(word);
}
for (var i = 0; i < words2.Count; i++)
{
Console.WriteLine(words2[i]);
}
words2[0] = "uno";
words2[0] = "due";
words2.Remove("uno");
words2.RemoteAt(0); // Remove an item at a specified index
var moreWords = new [] {"three", "four", "five"};
words2.addRange(moreWords); // add all the elements in a colleciton to the list
words2.indexOf("four"); // return -1 is element does not exist in the list
words2.Contains("five"); // return a bool
words2.Clear(); // remove all the elements in the listout keyword
Why we need this keyword? Sometimes we need to return 2 (or multiple) results from a method instead of one. There’re a couple of ways to do it. We could pack both of the values in some data structure and return them as a single object. The other way is to use out keyword.
List<int> GetOnlyPositive(int[] nums, out int countOfOnlyPositive)
{
countOfNonPositive = 0;
return new List<int>();
}
bool isParsingSuccessful = int.TryParse(userInput, out int number);If we use out modifier, we must assign some value to this parameter within this method. The out parameter must be assigned to before control leaves the current method. The parameter declared with the out modifier kind of works like the second result. 底层逻辑是pass by reference,也就是按引用传递。
Issues Caused By Procedural Programming
- Spaghetti Code: A messy, tangled code as hard to follow as following a single thread in a bowl of spaghetii. It is hard to move or remove a single thread from such a bowl without moving anything else. Similarly, it is hard to change anything in Spaghetti code.
- No way to control who can access methods
- No seperation by levels of abstraction
- Changes in requirements are hard to implement
- Logic is not easily configurable
Access Modifier
The default access modifier is private.
Public members use Pascal casing, means the first letter is in Capital. Private field names should start with an underscore followed by a lowercase letter.
Encapsulation
Bundling data with methods that operate on this data (in single class).
Constructor Overload
public MedicalAppointment(string patientName, int daysFromNow)
{
_patientName = patientName;
_date = DateTime.Now.AddDays(daysFromNow);
}
public MedialAppointment(string patientName) :
this(patientName, 7)
{
}Expression-bodied methods
When there’s only 1 expression or statement in method body, we can simplify with this:
public int CalculateCircumference() => 2 * Width + 2 * Height;Optional Parameters
public MedicalAppointment(string patientName, int daysFromNow = 7)
{
// constructor body
}The default value of an optional parameter must be compile-time constant, means it must be possible to evaluate it during the compilation, before the program is run. In practice, it means it can be of simple types like numbers, strings or bool. Another limitation is that the optional parameters must appear after all required parameters. It is possible to have multiple optional parameters. The methods with no optional parameters will always have precedence before the methods that do have optional parameters.
When using multiple optional parameters, we must remember that if we provide some of them, we must also provide all preceding optional parameters.
readonly vs const
A readonly field(只能修饰field,不能修饰property) can only be assigned at the declaration or in the constructor. If they are not, the default value will be used. After the object is constructed, it will not be possible to change the value of a readonly field. Making all fields of an object readonly makes the whole object immutable.
The const modifier can be applied to both variables and fields. Those variables and fields must be assigned at declaration and can never be modified afterward. Also, they must be given a compile-time constant value, so a value that can be evaluated during the compliation, before the application is run. When using the const modifier, we cannot use var. Const fields and variables should always be named staring with a capital letter, even if they are private.
We use readonly fields when we want a field never to change after it has been set in the constructor. But it is fine if the value is evaluated at runtime and passed as a constructor parameter. We use const fields for things with a constant value that known at compilation time.
Properties
Properties是一种比较高级且很有集成度的语法糖,用来替代繁琐的private field + getter + setter。Properties are actually more similar to methods than fields, and the way of naming them shows that.
private int _width;
public int Width // The name of property should always start with capital letter no matter it's private or not
{
get
{
return _width;
}
set
{
if (value > 0) // this is a special variable, and is simply the value that is being assigned to this property
{
_width = value;
}
}
}This is the old way for defining properties.
Object Initializer
If the properties of a class are publicly settable, we can use object initializers when creating new objects.
var person = new Person
{
Name = "John",
YearOfBirth = 1990
};
class Person
{
public String Name { get; set; }
public int YearOfBirth { get; set; }
}We don’t need to set all the properties when using object initializers. If we skip some, they will be just assigned a different value for their type. If the setters of those properties were not present or were priavte, using the object initializer would not be possible. Object initilizers only work if the properties have a public setter.
class Person
{
public String Name { get; set; }
public int YearOfBirth { get; init; }
}For “init” accessor, we can assign a value to the property only during object construction, so now we can use object initilization during the object creation, but after that it will work like the property does not have a setter at all.
Object Initilizer vs Constructor
When using a constructor, we must provide all the required parameters. With object initializer, we can choose what properties we set. Because the object initilizer forces us to specify the property names, it may be considered more readable, especially with many parameters.
Computed Properties
Sometimes properties don’t wrap any field but return a computed result instead. Computed properteis should never be performance-heavy.
public string Description => $"A rectangle with width: {Width} and height {Height}";Static
A static class cannot be instantiated; it only works as a container for methods. Static classes can only contain static methods. Non-static classes can contain static methods. But to call the static method, we must use the name of the class, not a specific instance.
static class Calculator
{
public static int Add(int a, int b) => a + b;
public static int Subtract(int a, int b) => a - b;
}All const fields are implicitly static, so we must access them as we access static fields using the class name, not the instance name.
In general, static fields and properties are used when we need to share a single member between all class instances. If we don’t initialize a static field or property, it will be automatically initialized to the default value.
Method Orders in A Class
- Public methods should be above private ones.
- If method A calls method B, A should be above B.
DRY – Don’t Repeat Yourself
DRY states that we shouldn’t have multiple places in the code where the sanem business decisions are defined. Following the DRY princlple does not necessarily mean that we should never ever have any code duplications. If 2 pieces of code that seem to be the same but implement unrelated business rules, it is fine to have them duplicated.
Files, Namespaces, Directives
We should always have one file for each class in a production code.
Namespaces declare a scope that contains a set of related types. Type not placed in any named namepsacce belongs to the global namespace. Namespaces names should match the folder structure. 先从project name开始,相当于是root folder,然后再加上subfoler等。
File scoped namespace declaration (starting from c# 10):
namespace Names_SingleResponsibilityPrinciple.DataAccess;
class StringTextualRepository
{
// code here
}“using” is a directive that can be used to import type.
using Names_SingleResponsibilityPrinciple.DataAccess;This way all types defined in this namespace will be available in this file. We must specify the full namespace name. 例如A.B, 如果我们只using A,那B是不会被导入的,这个是新手常犯的错误。Please remember that all using directives must always be at the very top of the file. 如果我们用到另一个class,虽然在另一个文件里,但属于同一个namespace的话,是不需要导入的。
global using 正是对整个项目(Project/Compilation Unit)生效的。
这是 C# 10 引入的一项非常实用的“减负”特性。它的核心逻辑是:你在项目中的任何一个 .cs 文件里写了 global using,那么这个命名空间就会自动注入到该项目下的所有文件中。唯一的要求就是global using要写在非global using的前面。
Random Seed
For the same seed, we will always get the same sequence of values. So it’s not really a random but rather a pseudorandom numbers generator. We can pass any numeric seed to the constructor of the random class. If no seed is passed to the constructor, the value of the seed is set to the number of ticks since the system started, where one tick is 100 nanoseconds.
var random1 = new Random();
var random2 = new Random();If we create 2 objects of the random class at almost the same moment in time, they may have the same seed and as a result give us the same sequence of numbers. This leads to some pretty common bugs when someone expects 2 random objects to generate different numbers, but in fact they generate the same numbers because they were created in the same window of 100 nanoseconds.
The best approach is to have a single Random object in the entire program.
Magic Number Antipattern
Magic Number(魔数) 是一种非常经典的代码异味(Code Smell),属于 反模式(Antipattern)。
简单来说,魔数是指在代码中直接出现的未经解释的数字字面量。它之所以被称为“魔”,是因为除了最初写代码的人,没人知道这个数字代表什么意思,它就像凭空变出来的一样。
Enum
An enum is a type that defines a set of named constants.
Season firstSeason = Season.Spring;
public enum Season
{
Spring,
Summer,
Autumn,
Winter
}Please notice that even if an enum declaration is a bit similar to a class definition, it cannot contain any fields, properties or methods.In the enum’s body, we only write the list of all possible values, the variables of this enum type can be assigned. Under the hood, each value of an enum is represented as an integer. By default, the first value is 0, the second one is 1, and so on. However, we can override this mapping:
public enum Season
{
Spring = 1, // 后面依次顺延,比如Summer是2,Autumn是3等等
Summer,
Autumn,
Winter
}
public enum HttpCode
{
OK = 20,
NotFound = 404,
InternalServerError = 500
}Inheritance
public class Ingredient
{
public virtual string Name { get; } = "Some ingredient";
}
public class TomatoSauce : Ingredient
{
public override string Name => "Tomota Sauce";
}Virtual
这个是Java没有的,所以仔细看。
GeneralType variable = new SpecificType();
variable.SomeMethod();When we have a variable of a specific type, but declare it as general type, and we call some method or property on it, the internal engine of c# does this: First, it checks if the method is virtual. If not, it simply calls the method it finds in the type of the variable. If the method is virtual, it checks the actual type of the object stored in the variable. In this case, the C# engine will call SomeMethod defined in this specificType if it overrides the base type method. If not, the implementation from the base class will be used. Virtual methods/properties may be overridden by the inheriting types. Dear C# engine, please check what object you actually have in the variable and call the method defined in this inheriting object if you find one. If not, call the method from the base type.
Casting
In most cases it is fishy to use any downcasting (父类转换为子类) in code. Usually using it indicats some serious problems with the design.
as Operator
Cheddar cheddar = (Cheddar)ingredient; // explictly casting
Cheddar cheddar = ingredient as Cheddar; // as operator
if (cheddar is not null)
{
Console.WriteLine(cheddar.Name);
}
else
{
Console.WriteLine("Convertion failed");
}Instead of converting the type with the cast expression, we can do it with the as operator. The difference between using cast expression and the as operator is that in the case of the unsuccessful cast, an exception will be thrown for the cast expression. For the “as” operator, the result will simply be null. Because the result may be null, it means we can only use this opeator to cast to those types that can be assigned null. We can only use casting with as when casting to a nullable type.
Abstract
Abstract class cannot be instantiated. They only serve as base classes for other more concrete types. Abstract methods can only be defined in abstract classes. They don’t have implementations so their bodies are empty. All abstract methods are implicitly virtual, and it must be overridden in non-abstract derived classes.
public abstract class Ingredient
{
public abstract void Prepare();
}Seal
如果一个class被seal了,那它就不能被继承了。
Only virtual, overridden methods can be sealed. 也就是说,子类不能再重写该方法了,重写到此为止。
All static classes are implicitly sealed so we can’t have classes derived from them.
Extension Methods
Extension methods allow us to seemingly add methods to an existing type without modifying this type’s source code. 不是真的把该方法加在要extend方法的类上。只是说从语法上来看,像是把该方法加在了那个类上。There’re several rules:
1. Extension methods can only be defined in static classes.
2. Extension method itself must always be static but they’re called as if they were instance methods on the extended type. The parameter the method takes if of the type we want to extend and it must always be the first parameter. If we want to add more parameters, they must be after it.
3. this modifier is crucial to make this method an extension method.
4. It is a good practice to have a seperate class for each extended type.
5. They also allow us to seemingly add methods to types that cannot have methods defined like enums.
namespace TodoList.Extensions;
public static class StringExtensions
{
public static int CountLines(this string str) => str.Split(Environment.NewLine).Length;
}To use it, we must import the namespace containing the extension method we added.
