Effective ways to improve C# code

Why should I change what can be used? Because the effect is better after the change. This is also true when developers switch to other tools or languages, because they work more efficiently after switching. If you refuse to change your existing habits, you will not appreciate the benefits of the new technology, but if the new technology looks very similar to the technology you are familiar with, it will be particularly difficult to change it. For example, C# language is similar to C++ or Java language. Because they both use a pair of curly braces to denote code blocks, even if developers switch to the C# language, they will always adopt the habit of using those two languages. Bring it over directly, this does not actually give full play to the advantages of C#. The first commercial version of this language was released in 2001. After these years of evolution, the difference between the current version of C# and C++ or Java is far greater than that of that era. If you are transferring to C# from another language, you need to learn the programming habits of C# language so that this language can promote your work, not hinder your work. This chapter will remind you to change those practices that are inconsistent with the C# programming style, and cultivate correct programming habits.

 

Article 1: Prefer implicitly typed local variables

Implicitly typed local variables are added to the C# language to support the anonymous type mechanism. The reason for adding this mechanism is that the result obtained by some query operations is IQueryable, while others return IEnumerable. If you insist on treating the former as the latter, you will not be able to use many of the enhancements provided by IQueryProvider (see Article 42). Using var to declare a variable without specifying its type can make developers focus more on the name and better understand its meaning. For example, the variable name jobsQueuedByRegion already clarifies the purpose of the variable. Even if its type Dictionary> is written out, it will not provide much help.

For many local variables, I like to use var to declare, because this can make people focus on the most important part, that is, the semantics of the variable, without distracting to consider its type. If the code uses inappropriate types, the compiler will warn you instead of worrying about it in advance. The type safety of a variable is not the same as whether the developer writes the type of the variable. In many cases, even if you bother to distinguish the difference between IQueryable and IEnumerable, developers can not obtain useful information. If you have to explicitly tell the compiler the type, sometimes it may change the way the code is executed (see item 42). In many cases, var can be used to declare implicitly typed local variables, because the compiler will automatically select the appropriate type. But this method cannot be abused, because it will make the code difficult to read, and may even produce subtle type conversion bugs.

The type inference mechanism of local variables does not affect C#'s static type checking. Why is this? First, we must understand that type inference for local variables is not equal to dynamic type checking. The variable declared with var is not a dynamic variable, its type will be determined according to the type of the value on the right side of the assignment symbol. The meaning of var is that you don't need to tell the compiler the type of the variable, the compiler will judge it for you.

The author now explains the benefits and problems of implicitly typed local variables from the perspective of whether the code is readable. In fact, in many cases, the type of local variables can be seen from the initialization statement:

Developers who understand C# can immediately understand what type of foo variable is when they see this statement. In addition, if a variable is initialized with the return value of a factory method, its type is usually obvious:

The name of some methods does not clearly indicate the type of return value, for example

This example is of course constructed deliberately by the author. When writing code, you should name the method so that the caller can infer the type of the return value. For the example just now, you only need to modify the name of the variable to make the code clear:

Although the method name itself does not indicate the type of the return value, after modifying it like this, many developers can infer from the name of the variable that the type of the variable should be Product.

The real type of the HighestSellingProduct variable is of course determined by the signature of the DoSomeWork method. Therefore, its type may not be Product itself, but a class inherited from Product, or an interface implemented by Product. In short, the compiler will determine the type of the HighestSellingProduct variable based on the signature of the DoSomeWork method. Regardless of whether its actual type at runtime is Product or not, as long as the type conversion operation is not explicitly performed, then the type judged by the compiler shall prevail.

Declaring variables with var may confuse people reading the code. For example, if you use the return value of the method to initialize such a variable as before, it will cause such problems. The person viewing the code will determine the type of the variable according to his own understanding, and the type he determined may exactly match the real type of the variable at runtime. However, the compiler will not consider the type of the object at runtime like people do, but will determine its type at compile time based on the declaration. If you directly indicate its type when declaring a variable, the compiler and other developers will see this type and will take the type as the standard. On the contrary, if you use var to declare, the compiler will infer its type. Other developers cannot see the type inferred by the compiler. Therefore, the type they determine may not match the type inferred by the compiler. This will cause the code to be incorrectly modified during the maintenance process and produce some avoidable bugs.

If the type of the implicitly typed local variable is a C# built-in numeric type, then there will be other problems, because when using such a numeric value, various forms of conversion may be triggered. Some conversions are widening conversions. This conversion is definitely safe. For example, from float to double, this is the case, but some conversions are narrowing conversions. This conversion will reduce the accuracy. For example, the conversion from long to int will cause this problem. If you clearly write the type that a numeric variable should have, you can better control it, and the compiler will also point you out where you might lose precision due to conversion.

Now look at this code:

What is the value of total? This question depends on the type of the return value of the GetMagicNumber method. The following 5 output results correspond to 5 GetMagicNumber versions, and the return value type of each version is different:

The total variable will show 5 different types in these 5 cases. This is because the type of the variable is determined by the variable f, and the type of the variable f is inferred by the compiler based on the return value type of GetMagicNumber() . When calculating the total value, some constants are used. Because these constants are written in literal form, the compiler will convert them to a type consistent with f and calculate them according to the rules of that type. Therefore, different types will produce different results.

This is not a defect of the C# compiler, because it just completes the task as usual according to the meaning of the code. Since the code uses implicitly typed local variables, the compiler will set the type of the variable itself, that is, make the best choice based on the part on the right side of the assignment symbol. Be careful when using implicitly typed local variables to represent values, because many implicit conversions may occur, which not only easily misunderstand people who read the code, but some of these conversions can also reduce accuracy.

This problem is certainly not caused by var, but because people who read the code do not know what type of return value of GetMagic-Number() is, nor do they know what default numeric conversions occur during operation. After removing the declaration statement of variable f, the problem still exists:

Even if the type of the total variable is clearly pointed out, the doubt cannot be eliminated:

Although the type of total is double, if GetMagicNumber() returns an integer value, the program will calculate the value of 100*GetMagicNumber()/6 according to the rules of integer arithmetic, and the decimal part cannot be stored in total.

The code is misleading because the developer cannot see the actual return type of GetMagicNumber(), nor can it easily observe the numerical conversion that occurs during the calculation.

If the return value of GetMagicNumber() is stored in a clearly typed variable, then this code will be easier to read, because the compiler will point out the mistakes made by the developer. When the return value type of GetMagicNumber() can be implicitly converted to the type of variable f, the compiler will not report an error. For example, when the method returns an int and the type of the variable f is decimal, such a conversion occurs. On the contrary, if the implicit conversion cannot be performed, a compilation error will occur, which will make the developer understand that he did not understand it correctly, and now he must modify the code. This way of writing allows developers to examine the code carefully to see the correct conversion method.

The example just now shows that the type inference mechanism of local variables may cause difficulties for developers to maintain code. Compared with the case where type inference is not used, the way the compiler operates in this case has not changed much. It will still perform the type checking that it should complete, but it is not easy for developers to see the relevant rules and values. Conversion behavior. In these occasions, the type inference mechanism of local variables has played a blocking role, making it difficult for developers to determine the relevant types.

But in other situations, the type selected by the compiler may be more appropriate than the type specified manually by the developer. The following simple code will take the customer name out of the database, then look for those names beginning with the string start, and save the query result in the variable q2:

This code has serious performance problems. The first line of the query statement will retrieve everyone’s name from the database. Because it needs to query the database, its return value is actually of type IQueryable, but the developer declares the variable q that holds the return value as IEnumerable type. Since IQueryable inherits from IEnumerable, the compiler will not report an error, but doing so will cause the subsequent code to be unable to use certain features provided by IQueryable. The next line of query statement was affected in this way. It could have used Queryable.Where to query, but it used Enumerable.Where. If the developer does not explicitly specify the type of variable q as IEnumerable, then the compiler can set it to a more appropriate IQueryable type. If IQueryable cannot be implicitly converted to IEnumerable, then the type of writing just now will cause the compiler to report an error. But in fact, implicit conversion can be completed, so the compiler will not report an error, which makes it easy for developers to ignore the performance problems caused by this.

The second query statement calls Enumerable.Where instead of Queryable.Where, which has a great impact on program performance. As mentioned in Article 42, IQueryable can combine multiple expression trees related to data query into one operation so that it can be executed at one time, and it is usually executed on the remote server where the data is stored. The second query statement of the code just now is equivalent to the where clause in the SQL query. Since the data source for this part of the query is IEnumerable type, the program will only include the first query statement. Some operations are performed on the remote computer. Next, you must first get all the customer names obtained from the database to the local, and then execute the second query statement (equivalent to the where clause in the SQL query) to search for the specified string and return the The result is consistent.

The following writing is better than the previous one:

This time the variable q is of type IQueryable, which is inferred by the compiler based on the return type of the first query statement. The C# system will combine the next query statement used to represent the Where clause with the first query statement to create a more complete expression tree. Only when the caller actually enumerates the content in the query result, the query operation represented by this tree will be executed. Since the expression used to filter the query results has been passed to the data source, the search results will only contain the contact names that match the filter criteria, which can reduce network traffic and improve query efficiency. This sample code is specially constructed by the author. If you encounter such a requirement in real work, you can simply combine the two statements into one. However, the situation demonstrated by this example is true, because you often encounter such requirements in work. Go to the place where you need to write multiple query statements in succession.

Compared with the code just now, the biggest difference between this code is that the type of variable q is no longer explicitly specified by the developer, but is inferred by the compiler, which makes its type change from the original IEnumerable to the current one. IQueryable. Since the extension method is a static method instead of a virtual method, the compiler will select the most matching invocation method according to the type of the object at compile time, instead of processing it according to its type at runtime, that is, here No late binding will occur. Even if there are instance members of the type at runtime that match this call, the compiler will not see them, so they will not be included in the candidate range.

Be sure to note: Since the extension method can see the runtime type of its parameters, it can create another set of implementations based on this type. For example, if the Enumerable.Reverse() method finds that its parameters implement the IList or ICollection interface, it will be executed in another way to improve efficiency (for this, please refer to Article 3 later in this chapter ).

When writing a program, if you find that the type automatically selected by the compiler may misunderstand the meaning of the code, making it impossible to immediately see the exact type of this local variable, then the type should be clearly pointed out, instead of using var to declare . Conversely, if the type predicted by the person reading the code according to the semantics of the code is consistent with the type automatically selected by the compiler, then it can be declared with var. For example, in the example just now, the variable q is used to represent the names of a series of contacts. People who see this initialization statement will definitely understand the type of q as a string. In fact, the type determined by the compiler It is also a string. Variables initialized by query expressions like this are usually of clearer type, so you might as well declare them with var. Conversely, if the expression used to initialize the variable cannot clearly convey the appropriate semantics, so that people who read the code can easily misunderstand its type, then var should not be used to declare the variable, but its type should be clearly stated. .

In short, unless the developer must see the declaration type of the variable in order to understand the meaning of the code correctly, otherwise, you can consider using var to declare local variables (the developer mentioned here also includes yourself, because you may also in the future To view the code written earlier). Note that the word I used in the title is priority, not always. This means that you cannot blindly use var to declare all local variables. For example, for numeric variables such as int, float, double, etc., you should clearly indicate its type. , And other variables may wish to use var to declare. Sometimes, even if you hit the keyboard a few more times to type the variable type, it may not ensure type safety or ensure that the code becomes easier to read. If you choose an inappropriate type, the efficiency of the program may decrease. The effect of doing so is not as good as letting the compiler automatically select it.

 

More C language-related VIP Exclusive free to read the article , nearly one thousand e-books for free reading, N, Download , one thousand courses free learning privileges,

↑↑ Open CSDN membership for free ↑↑
 

Poke me, open immediately>>    Enjoy 7 VIP special privileges!

Poke me and activate now >>

Guess you like

Origin blog.csdn.net/csdn_tuijian/article/details/113134449