C # The most error-prone problem for programmers in programming,
This article describes the 10 most common programming errors or the traps that C # programmers should avoid.
Common error 1: Use a reference or a reference like a value
Programmers in C ++ and many other languages are used to controlling whether the value they assign to a variable is a simple value or a reference to an existing object. In C #, this is determined by the programmer who writes the object, rather than by the programmer who instantiates the object and assigns values to the variable. This is a common "problem" for novice C # programmers ".
If you don't know whether the object you are using is a value type or a reference type, you may be pleasantly surprised. For example:
Point point1 = new Point(20, 30);Point point2 = point1;point2.X = 50;Console.WriteLine(point1.X); // 20 (does this surprise you?)Console.WriteLine(point2.X); // 50Pen pen1 = new Pen(Color.Black);Pen pen2 = pen1;pen2.Color = Color.Blue;Console.WriteLine(pen1.Color); // Blue (or does this surprise you?)Console.WriteLine(pen2.Color); // Blue
As you can see, although the Point and Pen objects are created in the same way, the value of point1 remains unchanged when a new X coordinate value is assigned to point2. When a new color value is assigned to pen2, pen1 also changes. Therefore, we can infer that both point1 and point2 contain copies of their Point objects, while pen1 and pen2 reference the same Pen object. How can we know this principle without this test?
One way is to see how the object is defined (in Visual Studio, you can place the cursor on the object name and press the F12 key)
public struct Point { … } // defines a “value” typepublic class Pen { … } // defines a “reference” type
As shown above, in C #, the struct keyword is used to define a value type, while the class keyword is used to define the reference type. For those who have a C ++ programming background, if they are confused by some similar keywords between C ++ and C #, they may be surprised by the above behavior.
If the behavior you want to depend on varies with the value type and reference type, for example, if you want to pass an object as a parameter to a method, modify the object status in this method. Make sure that you are processing the correct type object.
Common Error 2: misunderstanding the default value of uninitialized variables
In C #, the value type cannot be blank. According to the definition, the type value of the value or even the value type of the initialization variable must have a value. This is the default value of this type. This usually causes the following unexpected results to check whether a variable is not initialized:
class Program {static Point point1; static Pen pen1; static void Main(string[] args) {Console.WriteLine(pen1 == null); // TrueConsole.WriteLine(point1 == null); // False (huh?)}}
Why not [point 1] is null? The answer is that a point is a value type, which is the same as the default value (0, 0) and has no null value. Failed to realize this is a very simple and common error, in C #
Many (but not all) value types have an "IsEmpty" attribute. You can see that it is equal to the default value:
Console.WriteLine(point1.IsEmpty); // True
When you check whether a variable has been initialized, make sure that you know that the value is not initialized as the type of the variable. By default, the value is not null.
Common error 3: Use an inappropriate or unspecified method to compare strings
There are many methods in C # To compare strings.
Although many programmers use the = Operator to compare strings, this method is not recommended. The main reason is that this method does not specify the type used to compare strings in the code.
On the contrary, it is best to use the Equals method to determine whether the string is equal in C:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
The first Equals method (without the comparisonType parameter) returns the same result as the = Operator, but the advantage is that it explicitly specifies the comparison type. It compares strings by byte in sequence. In many cases, this is the expected comparison type, especially when comparing strings set through programming, such as file names, environment variables, and attributes. In these cases, only the byte-by-byte comparison can be performed. The only bad thing about comparing using the Equals method without the comparisonType parameter is that those who read your program code may not know what the comparison type is.
Using the Equals method with comparisonType to compare strings not only makes your code clearer, but also makes you think about which type to compare strings. This method is worth using, because although there are not many differences between the sequential comparison and the comparison by language region in English, however, some other languages may be quite different. If you ignore this possibility, you will undoubtedly dig a lot of traps for yourself in the future ". For example:
string s = "strasse";// outputs False:Console.WriteLine(s == "straße");Console.WriteLine(s.Equals("straße"));Console.WriteLine(s.Equals("straße", StringComparison.Ordinal));Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture));Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase));// outputs True:Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture));Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
The safest practice is to always provide a comparisonType parameter for the Equals method.
The following are some basic guiding principles:
When comparing user input strings or displaying string comparison results to users, use localized comparison (CurrentCulture or CurrentCultureIgnoreCase ).
When comparing strings for programming, use the original comparison (Ordinal or OrdinalIgnoreCase)
InvariantCulture and InvariantCultureIgnoreCase are generally not used unless in a restricted situation, because the original comparison is usually more efficient. If the comparison related to local culture is essential, it should be executed based on the comparison of the current culture or another special culture.
In addition, for the Equals method, the string usually provides the Compare method, which can provide the relative sequence information of the string rather than simply testing for equality. This method applies well to the <, <=,> and> = operators.
Common Mistakes 4: Use iterative statements instead of declarative statements to operate the set
In C #3.0, the introduction of LINQ changes the previous query and modification operations on the set objects. From this point on, you should use LINQ to operate the set, rather than iteration.
Some C # programmers do not even know the existence of LINQ. Fortunately, the number of people who do not know is gradually decreasing. However, some people mistakenly think that LINQ is only used for database queries, because the keyword of LINQ and SQL statements are too similar.
Although the database query operation is a typical application of LINQ, it can also be applied to various enumerated set objects. (For example, any object that implements the IEnumerable interface ). For example, if you have an array of the Account type, do not write it as follows:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") {total += account.Balance;}}
You only need to write as follows:
decimal total = (from account in myAccountswhere account.Status == "active"select account.Balance).Sum();
Although this is a simple example, in some cases, a single LINQ statement can easily replace dozens of statements in an iteration loop (or nested loop) in your code. Less Code usually means fewer bugs are introduced. However, remember, you may have to weigh the performance. In scenarios where performance is critical, especially when your iterative code can make assumptions about your set, you must compare the performance between the two methods.
Common Errors: underlying objects are not taken into account in the LINQ statement.
For processing abstract operations on a set of tasks, LINQ is undoubtedly large. Whether they are in-memory objects, database tables, or XML documents. In such a perfect world, you do not need to know the underlying objects. The mistake here is to assume that we live in a perfect world. In fact, the same LINQ statement can return different results. When executed on the exact same data, if the data happens to be in a different format.
For example, consider the following statement:
decimal total=(from accout in myaccoutswhere accout.status==‘active"select accout .Balance).sum();
Imagine what will happen to the account of one of the objects. The status is equal to "valid" (note the upper case of )?
Well, if myaccout is a Dbset object. (Different case-sensitive configurations are set by default), The where expression will still match this element. However, if myaccout is in the memory array, it will not match and thus produce different overall results.
Wait a moment. In the string comparison we have discussed before, we can see that the = Operator plays a simple comparison. so why does = show another form under this condition?
The answer is: when the basic objects in the LINQ statement reference data in the SQL table (for example, in this example, if the object framework is a DbSet object ), the statement is converted into a T-SQL statement. Then follow the T-SQL rules instead of the C # rules, so the end of the comparison above is case insensitive.
In general, even if LINQ is a useful and consistent way to query a set of objects, in reality, you also need to know whether your statement will be translated into any engine or other expressions than C # To ensure that the behavior of your code will be as expected at runtime.
Common Error 6: be confused about the extension method or be cheated by its form
As mentioned earlier, the LINQ state depends on the Implementation object of the IEnumerable interface. For example, the following simple functions aggregate the account balance in the account set:
public decimal SumAccounts(IEnumerable myAccounts) { return myAccounts.Sum(a => a.Balance);}
In the above Code, the type of the myAccounts parameter is declared as IEnumerable, and myAccounts references a Sum method (C # uses a similar "dot notation" to reference classes in methods or interfaces ), we expect that Defines a Sum () method in the interface. However, IEnumerable No reference is provided for the Sum method and only the following concise definitions are provided:
public interface IEnumerable : IEnumerable {IEnumerator GetEnumerator();}
But where should the Sum method be defined? C # is a strongly typed language. Therefore, if the reference of the Sum method is invalid, the C # compiler reports an error to it. We know it must exist, but where should it be? In addition, where are all methods provided by LINQ for query and aggregation results defined?
The answer is that Sum is not defined in the IEnumerable interface, but a static method ("extension method") defined in the System. Linq. Enumerable class ")
namespace System.Linq {public static class Enumerable { ...// the reference here to “this IEnumerable source” is// the magic sauce that provides access to the extension method Sumpublic static decimal Sum(this IEnumerable source,Func selector); ...}}
But what is the difference between the extension method and other static methods? What makes sure that we can access it in other classes?
The obvious feature of the extension method is the this modifier before the first parameter. This is how the compiler knows that it is an extension method ". The type of the parameter it modifies (in this example, IEnumerable ) Indicates that this class or interface will implement this method.
(It should also be noted that the similarity between the IEnumerable interface defining the extension method and the Enumerable class name is not surprising. This similarity is just a casual style choice .)
With this understanding, we can see that the sumAccounts method described above can be implemented in the following ways:
public decimal SumAccounts(IEnumerable myAccounts) {return Enumerable.Sum(myAccounts, a => a.Balance);}
As a matter of fact, we may have implemented this method in this way, rather than asking about the need for an extension method. The extension method itself is just a C # method that allows you to "add" methods of the existing type without inheriting, re-compiling, or modifying the original code.
The extension method is introduced to the scope by adding using [namespace] at the beginning of the file. You need to know the namespace of the extension method you are looking. This is easy if you know what you are looking.
When the C # compiler calls a method when it encounters an object instance, and it cannot find that method in the class of this object, it will try to find a matching class and method signature in all the extension methods in the scope. If it finds the extension method, it passes the instance reference as the first parameter to the extension method. If there are other parameters, it passes them into the Extension Method in turn. (If the C # compiler does not find the corresponding extension method in the scope, it will throw .)
For the C # compiler, the extension method is a "syntactic Sugar", so that we can write the code more clearly and make it easier to maintain (in most cases ). Obviously, the premise is that you know its usage. Otherwise, it will be confusing, especially at the beginning.
The application extension method does have advantages, but it also wastes time for developers who do not know it or do not know it correctly. Especially when you look at the sample code online or other code that has already been written. When the code produces a compilation error (because it calls methods that are clearly not defined in the called type), the general tendency is to consider whether the Code is applied to other versions of the referenced class library, or even different class libraries. A lot of time will be spent on finding new versions or class libraries that are considered "lost.
The extension method name is the same as the method name defined in the class. When there are slight differences in the method signature, Even developers familiar with the extension method occasionally make the above errors. A lot of time will be spent on finding a spelling mistake that "does not exist.
In C #, expansion methods are becoming increasingly popular. In addition to LINQ, the extension methods are also applied in two libraries, Unity Application Block and Web API framework, which are widely used by Microsoft. The newer the framework, the more likely it is to use an extension method.
Of course, you can also write your own extension method. But you must realize that although the extension method looks to be called like other instance methods, this is actually a phantom. In fact, the extension method cannot access the private and protected members of the extended class, so it cannot be used as a substitute for traditional inheritance.
Common error 7: incorrect set type for tasks on the opponent's head
C # provides a large number of set-type objects. Only one part is listed below:
Array,ArrayList,BitArray,BitVector32,Dictionary,HashTable,HybridDictionary,List,NameValueCollection,OrderedDictionary,Queue, Queue,SortedList,Stack, Stack,StringCollection,StringDictionary.
However, in some cases, too many choices are as bad as not enough, and the same is true for set types. A large number of options can certainly ensure that your work works properly. However, you 'd better spend some time searching and learning about the collection type in advance to select a collection type that best suits your needs. This will eventually improve your program performance and reduce the possibility of errors.
If the element type specified by a set (such as string or bit) is the same as that you are operating on, you 'd better use it first. When the corresponding element type is specified, this set is more efficient.
In order to make good use of the type security in C #, you 'd better choose to use a generic interface instead of an excuse for using a non-generic interface. The element type in the generic interface is the type you specify when declaring the object, and the element in the non-generic interface is the object type. When using a non-generic interface, the C # compiler cannot perform type checks on your code. Similarly, when you operate a collection of native types, using non-generic interfaces will cause C # To perform frequent boxing and unboxing operations on these types. This will have a significant performance impact compared to using a set of generic types specified.
Another common trap is to implement a set type by yourself. This does not mean never to do this. You can save a lot of time by using or extending some of the widely used Collection types provided by. NET, rather than repeating the wheel. In particular, C #'s C5 Generic Collection Library and CLI provide many additional Collection types, such as persistent tree data structures, heap-based priority queues, and hash Index Array lists, linked List and more.
Common error 8: release of missing resources
The CLR hosting environment plays the role of the garbage collector, so you do not need to explicitly release the memory occupied by the created object. In fact, you cannot release it explicitly. In C #, there is no operator corresponding to C ++ delete or a method corresponding to the free () function in C language. However, this does not mean that you can ignore all used objects. Many object types encapsulate many other types of system resources (such as disk files, data connections, network ports, and so on ). Keeping these resources in use results in sharp depletion of system resources, compromising performance and eventually causing program errors.
Although all C # Classes define the destructor, the possible problem with destroying objects (C # is also called Terminator) is that you are not sure they will be called. They will be called by the garbage collector in an uncertain future (an asynchronous thread, which may lead to additional concurrency ). Try to avoid GC in the garbage collector. the forcible restrictions imposed by the Collect () method are not a good programming practice, because it may cause thread congestion in an unpredictable period of time when the garbage collection thread tries to recycle objects that are suitable for recycling.
This also means that it is better not to use the Terminator. Releasing resources explicitly will not cause any consequence. When you open a file, network port, or data connection, when you stop using these resources, you should release these resources explicitly as soon as possible.
Resource leakage is a concern in almost all environments. However, C # provides a robust mechanism to simplify resource usage. If it is used properly, it can greatly reduce the probability of leakage. NET framework defines an IDisposable interface, which consists of only one Dispose. Any interface that implements IDisposable will call the Dispose () method at the end of the object lifecycle. The call result is clear and decisive.
If you create and release an object in a code segment but forget to call the Dispose () method, because C # provides using statements to ensure that the Dispose () method is called no matter how code exits (whether it is an exception, return statement, or a simple code segment ends ). This using is the same as the previously mentioned introduction of namespace at the beginning of the file. It has another objective that many C # developers are unaware of, completely unrelated, that is, to ensure that the Dispose () method of the object is called when the code exits:
using (FileStream myFile = File.OpenRead("foo.txt")) {myFile.Read(buffer, 0, 100);}
Using the using statement in the preceding example, you can determine that the myFile. Dispose () method will be called immediately after the file is used, regardless of whether the Read () method throws an exception.
Common error 9: avoidance exception
C # The type check is also mandatory during running. Compared to a language that assigns a random value to an incorrect type conversion like C ++, C # allows you to locate the error location more quickly. However, programmers once again ignore this feature of C. Because C # provides two types of check methods, one will throw an exception, and the other will not, which may cause them to fall into this "pitfall. Some programmers tend to avoid exceptions and think that not writing try/catch statements can save some code.
For example, the following shows two different display type conversion methods in C:
// Method 1: // if the account cannot be converted to a SavingAccount, an exception SavingsAccount savingsAccount = (SavingsAccount) account is thrown; // Method 2: // if the account cannot be converted, no exception is thrown, instead, it returns nullSavingsAccount savingsAccount = account as SavingsAccount;
Obviously, if you do not judge the result returned by method 2, it is likely that an NullReferenceException will eventually be generated, which may occur later, making it more difficult to track the problem. In comparison, method 1 immediately throws an InvalidCastExceptionmaking. In this way, the root cause of the problem is obvious.
In addition, even if you know that you want to judge the return value of method 2, if you find that the value is null, what will you do next? Is it appropriate to report errors in this method? If the type conversion fails, do you have other methods to try? If not, throwing this exception is the only correct choice, and the exception throw point is closer to its occurrence point, the better.
The following example demonstrates another common method, one that throws an exception, and the other that does not:
Int. parse (); // If the parameter cannot be parsed, an int exception is thrown. tryParse (); // The returned bool value indicates whether the resolution is successful. IEnumerable. first (); // If the sequence is null, an exception IEnumerable is thrown. firstOrDefault (); // If the sequence is null, null or default value is returned.
Some Programmers think that "exceptions are harmful", so they naturally think that programs that do not throw exceptions are even more "advanced ". Although this is true in some cases, it does not apply to all situations.
For example, if an exception occurs in some cases and you have another option (for example, the default value), it is a good choice to choose not to throw an exception. In this case, you write as follows:
If (int. tryParse (myString, out myInt) {// use myInt} else {// use default value} Instead of: try {myInt = int. parse (myString); // use myInt} catch (FormatException) {// use default value}
However, this does not mean that the TryParse method is better. Applicable in some cases, but not in some cases. This is why two methods are available. Select an appropriate method based on your actual situation, and remember that, as a developer, exceptions can completely become your friends.
Common error 10: Accumulate compiler warnings without processing
This error is not unique to C #, but in C #, there are many such cases, especially after the C # compiler discards strict type checks.
There is a reason for warning. All C # compilation errors indicate that your code is defective. Likewise, some warnings are the same. The difference between the two lies in that for warnings, the compiler can work according to your code instructions, but the compiler finds that your code has a small problem, it is likely that your code will not run as expected.
A common example is that you modified your code and removed the use of some variables, but you forgot to remove the declaration of the variable. The program can run well, but the compiler will prompt that there are unused variables. The program can run well so that some programmers do not fix the warning. Furthermore, some programmers use the hidden warning function of the "Error List" window in Visual Studio to easily filter out the alarms so as to focus on errors. It does not take long to accumulate a bunch of warnings, which are ignored by "cool" (or even worse, hidden ).
However, if you ignore such warnings, the following example will appear in your code sooner or later.
Class Account {int myId; int Id; // the compiler has warned you, but you do not listen. // constructsponcount (int id) {this. myId = Id; // OOPS !}}
Coupled with the editor's smart sensing function, this error is very likely to happen.
Now, your code has a serious error (but the compiler only outputs a warning, which has already been explained), which wastes a lot of time to find this error, the specific situation is determined by the complexity of your program. If you have noticed this warning at the beginning, you can modify it in 5 seconds to avoid this problem.
Remember, if you look at it carefully, you will find that the C # compiler gives you a lot of useful information about your program robustness. Do not ignore warnings. You only need to spend a few seconds to fix them. You can fix them when they appear, which saves you a lot of time. Try to cultivate a kind of cleanliness for yourself, so that Visual Studio always displays "0 errors, 0 warnings" in the "error window". Once a warning appears, it will feel uncomfortable, then immediately fix the warning.
Of course, there are exceptions to any rule. So sometimes, although your code is a bit problematic in the compiler, it is exactly what you want. In this rare case, you 'd better use # pragma warning disable [warning id] to wrap the code that triggers the warning, and only wrap the code corresponding to the warning ID. This will only suppress the corresponding warnings, so you will still know when new warnings are generated ..
C # is a powerful and flexible language. It has many mechanisms and language specifications to significantly improve your productivity. Like other languages, if you have a limited understanding of its capabilities, this may hinder you, not benefit. As the saying goes, "knowing enough to be dangerous" (Note: I think I know enough, but I can do something, but it is not ).
Familiar with some key nuances of C #, such as those mentioned in this article (but not limited to these), can help us better use the language and avoid some common traps.