[Translation] you in the end how proficient in C #?

Even if the code is C # developers with good writing skills may sometimes unexpected behavior. This article describes some C # code snippets that category, and explain the reasons behind the surprising behavior.

I see Home Introduction free C ++ learning resources, video tutorials, career planning, interview Detailed, learning routes, development tools

Live 8 o'clock every night to explain the C ++ programming techniques.

Null value

We all know that, if not handled properly, null (null) can be dangerous.

Using a null object (e.g., a null call methods on the object, or to access one of its properties) can result in  a NullReferenceException  , for example:

object nullValue = null;

bool areNullValuesEqual = nullValue.Equals(null);

For security purposes, we need to make sure before using reference types are not null. Failure to do so may result in a particular edge untreated abnormal. Although such errors occasionally occur in every person, but we can hardly be called unexpected behavior.

However, the following code?

string null string = (string) zero;

bool isStringType = nullString is string;

What is the value isStringType? Explicitly declare a variable string as a string of whether it will type at run time?

The correct answer is:  No

null values ​​at run time is not of the type

To some extent, this will also affect reflection. Of course, you can not call GetType on an empty value (), as it will lead to a null reference exception:

object nullValue = null;

Type nullType = nullValue.GetType();

Next, we look at the type of value can be empty

int intValue = 5;

Nullable<int> nullableIntValue = 5;

bool areTypesEqual = intValue.GetType() == nullableIntValue.GetType();

It can be used to distinguish the reflective type and is not null null type?

The answer is:  not

In the above code return the same type of two variables:  System.Int32 The  . However, this does not mean that there is no reflection on the Nullable representation.

Type intType = typeof(int);

Type nullableIntType = typeof(Nullable<int>);

bool areTypesEqual = intType == nullableIntType;

The snippet types are different. As expected, the nullable type with  [[System.Int32] System.Nullable'1 FIG. Only when the check value, the value will be treated as non-null value in reflection.

17556386-901c84a034021cbd.gif

A null value overloaded methods

Before turning to other topics, let us carefully to understand how to handle null values ​​when calling the same number of parameters but different types of overloaded methods.

private string OverloadedMethod(object arg)

{

return "object parameter";

}

private string OverloadedMethod(string arg)

{

return "string parameter ";

}

If we use empty (null) value call this method, what happens?

var result = OverloadedMethod(null);

Which overloads the call? Or code because the method invocation is ambiguous and does not compile?

In this case, the  code can be compiled  , and invoke method string parameter.

Typically, when a parameter type can be converted into a parameter type (i.e., a parameter type is derived from another parameter type), the code can be compiled. More specifically, the method calls the parameter type.

When not convert between these two types, the code will not compile.

To force call a specific overloaded, you can cast a null value for this parameter type:

var result = parameteredMethod((object)null);

17556386-f565ca5de78751d5.gif

Arithmetic

Most of us do not often use bit shift operation.

Let us refresh memory. Left shift operator (<<) to move to the left of a given binary representation of the number of positions:

var shifted = 0b1 << 1; // = 0b10

Similarly, right shift (>>) form the binary representation moves to the right:

var shifted = 0b1 >> 1; // = 0b0

When these bits (bit) reaches the end, they will not wrap (wrap). This is why the results of the second expression is 0. If we move far enough bits left (32-bit, 32-bit numbers as integers), the same will happen:

var shifted = 0b1;

for (int i = 0; i < 32; i++)

{

shifted = shifted << 1;

}

The result will be zero again.

However, a bit shift operator having a second operand. We can move 32 to the left, rather than left to move one 32 times and got the same results.

var shifted = 0b1 << 32;

Is that right? This is wrong!

The result of this expression will be 1. why?

Because this embodiment is the definition of the operator. Prior to the application operation, the second operand using analog operand is a normalized length homing operation, i.e. calculated by dividing by a second operand bit length of the remaining portion of the first operand.

The first operand in the example we have just seen that the 32-bit number, thus: 32% 32 = 0. Our numbers will move 0 to the left. This and move it a 32 is not the same.

Let us continue to operate  &  (and)  |  (or). The type of the operands, they represent two different operations:

For Boolean operands, they serve as a logical operator, && and || similar, there is a difference: they are hungry (eager), i.e. always calculated two operands can be determined even if the first operand after evaluation result.

For integer type, which act as a logical bitwise operators, typically used to represent an enumeration type of Flag.

[Flags]

private enum Colors

{

None = 0b0,

Red = 0b1,

Green = 0b10,

Blue = 0b100

}

| Operator for combining flag (Flag), & operator is used to check whether the flag:

Colors color = Colors.Red | Colors.Green;

bool isRed = (color & Colors.Red) == Colors.Red;

In the above code, I bitwise logical operations before and after the parentheses to make clearer the code. The need for parentheses in this expression?

It turns out,  yes  .

With different arithmetic operators, bitwise logical operators lower priority than the equality operator. Fortunately, due to the type checking, no parentheses code will not compile.

From the .NET Framework 4.0 onwards, there is a better alternative methods can be used to check mark, you should always use it, rather than the & operator:

bool isRed = color.H asFlag(Colors.Red);

Math.Round()

We continue to talk to Round for example arithmetic operations. How it is the midpoint between the (e.g., 1.5) in the rounded value two integer values? Up or down?

var rounded = Math.Round(1.5);

If you predict is 2, you're right. The result would be 2. This is the general rule?

var rounded = Math.Round(2.5);

Do not. The result is 2 again. By default, the midpoint Round to the nearest even number. You can provide the second parameter is the method to explicitly request such acts:

var rounded = Math.Round(2.5, MidpointRounding.ToEven);

You can change behavior using a second, different values ​​of the parameters:

var rounded = Math.Round(2.5, MidpointRounding.AwayFromZero);

With this clear rules, comes now always rounded up.

Rounding floating-point numbers are also affected accuracy.

was value = 1.4f;

var rounded = Math.Round(value + 0.1f);

While the midpoint be rounded to the nearest even number, i.e. 2, but in this case, the result will be 1, because for single precision floating point, no precise 0.1 representation, actually less than the calculated figure 1.5 and therefore to Round 1.

Although this particular problem does not occur when using double-precision floating-point numbers, but rounding errors may still occur, although less frequently. Therefore, when the maximum precision required, it should always be used rather than a floating decimal or double precision.

17556386-4fefcd39fb9e5522.gif

Class initialization

Best practice recommends avoiding class constructor to initialize the class as much as possible, to prevent abnormal.

All of which are more important for static constructor is.

As you probably know, when we try to instantiate static constructor at run time, it called before an instance constructor.

This is the initialization sequence when any class instantiation:

Static fields (first class only access: static member or the first instance)

Static constructor (first class only access: static member or the first instance)

Examples of fields (each instance)

Instance constructors (each instance)

Let's create a class has a static constructor, you can configure it to throw an exception:

public static class Config

{

public static bool ThrowException { get; set; } = true;

}

public class FailingClass

{

static FailingClass()

{

if (Config.ThrowException)

{

throw new InvalidOperationException();

}

}

}

Any attempt to create an instance of this will result in an exception, this should not come as a surprise:

var instance = new FailingClass();

However, it will not be  InvalidOperationException   . Runtime will automatically packaged into  TypeInitializationException   in. If you want to catch the exception and recover, it is important to note details.

try

{

var failedInstance = new FailingClass();

}

catch (TypeInitializationException) { }

Config.ThrowException = false;

var instance = new FailingClass();

Application of knowledge we have learned, the above code should catch exceptions caused by static constructor, change the configuration to avoid raising an exception in subsequent calls, finally succeeded in creating an instance of the class, right?

Unfortunately, no.

Static constructor for a class called only once. If it throws an exception, whenever you want to create an instance or in any other way to access the class, will be re-trigger this exception.

Before restart the process (or application domains), the class can not actually use. Yes, even the possibility of a static constructor throws an exception of a very small, is a very bad idea.

Initialization sequence derived class

For derived classes, the initialization sequence is more complex. In borderline cases, it may give you trouble. It is time to do a contrived example of:

public class BaseClass

{

public BaseClass()

{

VirtualMethod (1);

}

public virtual int VirtualMethod(int dividend)

{

return dividend / 1;

}

}

public class DerivedClass : BaseClass

{

int divisor;

public DerivedClass()

{

divisor = 1;

}

public override int VirtualMethod(int dividend)

{

return base.VirtualMethod(dividend / divisor);

}

}

Can you find a problem in the derived class do? When I try to instantiate it, what happens?

var instance = new DerivedClass();

It will lead to a DivideByZeroException. why?

The reason is the initialization sequence derived class:

Firstly, the furthest instance field according to the order of the base class is derived from the initialized.

Secondly, in order to call the constructor furthest from the base class to the derived class.

Since the whole initialization process, the class is considered  DerivedClass  , we  BaseClass  call the constructor  VirtualMethod  implementation of this method is actually realized in the DerivedClass, this time  DerivedClass  constructor has a chance to initialize  divisor  field. This means that the value is still zero, which leads to  DivideByZeroException  .

In our example, you can directly initialize divisor field rather than in the constructor to solve this problem through.

However, this example illustrates why can be dangerous from the constructor calls a virtual method. When you call them, they may not call the constructor of the class defined, they may exhibit unexpected behavior.

Polymorphism

Polymorphism is the ability of different classes implement the same interface in a different way.

However, we usually expect a single instance always use the same method to achieve, whether it is by what type of cast. This can be set as the base class, and invokes a particular method in the collection of all instances to a specific method implemented for each type to be called.

Examples down before conversion Having said that, but when we call this method, you can come up with a way to call a different way? (Ie break polymorphic behavior)

var instance = new DerivedClass();

var result = instance.Method(); // -> Method in DerivedClass

result = ((BaseClass)instance).Method(); // -> Method in BaseClass

The correct answer is: by using the  new  modifier.

public class BaseClass

{

public virtual string Method()

{

return "Method in BaseClass ";

}

}

public class DerivedClass : BaseClass

{

public new string Method()

{

return "Method in DerivedClass";

}

}

This base class from which hidden  DerivedClass.Method  , so when converting the call to the base class instance  BaseClass.Method  .

This applies to the base class, the base class can have their own methods. For an interface can not contain their own methods to achieve, you can come up with a method to achieve the same goal it?

var instance = new DerivedClass();

var result = instance.Method(); // -> Method in DerivedClass

result = ((IInterface)instance).Method(); // -> Method belonging to IInterface

It is an explicit interface

public interface IInterface

{

string Method();

}

public class DerivedClass : IInterface

{

public string Method()

{

return "Method in DerivedClass";

}

string IInterface.Method()

{

return "Method belonging to IInterface";

}

}

It is typically used to implement its user interface method hidden class, unless they are converted to instances of the interface. However, if we want to have two different methods in a single class to achieve its effects are just as good. However, it is difficult to come up with a good reason to do it.

Iterator

Iterator is a structural configuration of the set of single-step, it is generally used  foreach  statement. They consist  IEnumerable <T>  represents a type.

Although they are easy to use, but because of some magic compiler, if we can not very well understand the inner workings, we will soon fall into the trap of incorrect usage.

Let's look at this example. We will call a method that returns an IEnumerable from inside using:

private IEnumerable<int> GetEnumerable(StringBuilder log)

{

using (var context = new Context(log))

{

return Enumerable.Range(1, 5);

}

}

Of course,  Context  type implements  IDisposable  . It writes a message to the log to indicate when the input and out of its scope. In the actual code, in this context it can be replaced by a database connection. In it, the embodiment will flow from the result set returned by the read row.

public class Context : IDisposable

{

private readonly StringBuilder log;

public Context(StringBuilder log)

{

this.log = log;

this.log.AppendLine("Context created");

}

public void Dispose()

{

this.log.AppendLine("Context disposed");

}

}

To use  GetEnumerable  return value, we use the  foreach  loop:

were log = new String Builder ();

foreach (var number in GetEnumerable(log))

{

log.AppendLine($"{number}");

}

After the code is executed, the log will be what is? Whether the value returned will be listed between context creation and disposal?

No, they will not:

Context created

Context disposed

1

2

3

4

5

This means that our actual database example, the code will fail - before reading values ​​from the database, the connection is closed.

How do we fix the code so that released only after all values ​​have been in the context of iterations?

The only way to do this is to access the collection cycle GetEnumerable method:

private IEnumerable<int> GetEnumerable(StringBuilder log)

{

using (var context = new Context(log))

{

foreach (var i in Enumerable.Range(1, 5))

{

yield return i;

}

}

}

When we iterate through the returned  IEnumerable  , the context will only be released as expected at the end:

Context created

1

2

3

4

5

Context disposed

If you are not familiar with the  yield return  statement, which is syntactic sugar for creating state machines, allowing incremental manner using the code execute its methods, because the resulting  IEnumerable  being iteration.

This can be better explained following method:

private IEnumerable<int> GetCustomEnumerable(StringBuilder log)

{

log.AppendLine("before 1");

yield return 1;

log.AppendLine("before 2");

yield return 2;

log.AppendLine("before 3");

yield return 3;

log.AppendLine("before 4");

yield return 4;

log.AppendLine("before 5");

yield return 5;

log.AppendLine("before end");

}

To see the behavior of this code, we can use the following code to iterate through them:

were log = new String Builder ();

log.AppendLine("before enumeration");

foreach (var number in GetCustomEnumerable(log))

{

log.AppendLine($"{number}");

}

log.AppendLine("after enumeration");

Let's look at the contents of the log after the code is executed:

before enumeration

before 1

1

before 2

2

before 3

3

before 4

4

before 5

5

before end

after enumeration

We can see that for each value we traversed, two  yield return  code that will be executed between the statements.

For the first value, which is from the beginning to the method a  yield return  statement code. For the second value, it is the first and second  yield return  codes between statements. And so on, until the end of the method.

When the  foreach  loop checks after the last iteration of the loop  IEnumerable  when the next values, will be called the last  yield return  code following statement.

It is also worth noting that every time we pass  IEnumerable  iteration, will execute this code:

were log = new String Builder ();

var enumerable = GetCustomEnumerable(log);

for (int i = 1; i <= 2; i++)

{

log.AppendLine($"enumeration #{i}");

foreach (var number in enumerable)

{

log.AppendLine($"{number}");

}

}

After executing this code, the log will have the following content:

enumeration #1

before 1

1

before 2

2

before 3

3

before 4

4

before 5

5

before end

enumeration #2

before 1

1

before 2

2

before 3

3

before 4

4

before 5

5

before end

In order to prevent Every time we execute code through an iterative IEnumerable, IEnumerable is best to store the result of a local collection (eg, list), if we plan to use it many times, read it from there:

were log = new String Builder ();

var enumerable = GetCustomEnumerable(log).ToList();

for (int i = 1; i <= 2; i++)

{

log.AppendLine($"enumeration #{i}");

foreach (var number in enumerable)

{

log.AppendLine($"{number}");

}

}

Now, the code will be executed only once - when we create a list, then its iterations:

before 1

before 2

before 3

before 4

before 5

before end

enumeration #1

1

2

3

4

5

enumeration #2

1

2

3

4

5

When we're iteration  IEnumerable  when followed by a slow I / O operations, which is particularly important. Database access is a typical example.

Did you correctly predict the behavior of all the article an example?

If not, you may have learned that when you are not completely sure how a particular function is implemented, taking behavior can be dangerous. Impossible to know and remember every edge case in one language, so when you are unsure of some important code encounters, it is best to check the document or their first try.

More importantly, any one of which is to avoid writing may make other developers surprised code (or even possible that you after a certain time). Try the default value (as in the example of our Math.Round) write it in a different way or pass the optional parameter so that the intent clearer.

If this does not work, write test methods. They will clearly record the expected behavior!

You can correctly predict which? Let us know in the comments.

Yacoub Masd a technical review of this article.

Suprotim Agarwal on this article editorial review.

Guess you like

Origin blog.csdn.net/weixin_34375251/article/details/90792459