44. Complex numbers

The ComplexNumber structure is fairly long, so I'll describe it in pieces.

You could make ComplexNumber either a class or a structure. Many programmers use classes exclusively. This isn't a terrible choice, in part because it frees you from having to deal with the differences between classes and structures.

I’ve decided to make ComplexNumber a structure for a couple of reasons. Microsoft's guidelines generally recommend using a structure if the item meets the following criteria:

  • It is relatively small
  • It logically represents a primitive type such as a number
  • It is immutable, so its value does not change after it has been created
  • It will not need to be boxed and unboxed often

A ComplexNumber clearly meets the first two criteria. The .NET Framework is good at allocating and deallocating small objects, so making the class immutable shouldn't hurt performance.

Finally, if you typically use a ComplexNumber for numeric calculations, then you shouldn't need to box and unbox it very often.

Having decided to make this a structure instead of a class, let's look at the structure's requirements. The structure uses the following declaration to indicate that it will implement the IEquatable interface:

public struct ComplexNumber : IEquatable<ComplexNumber>

I'll show you how the structure implements the interface later.

The structure uses the following code to store a number's real and imaginary parts.

// Auto-implemented properties.
private double Re { get; }
private double Im { get; }

These properties are read-only because they have no set accessors. This means that a program cannot change a ComplexNumber structure's value after the structure has been created. In fact, even the structure's private code cannot change the properties' values. This makes the structure immutable.

This is a structure, so if you create a ComplexNumber without using a constructor, its real and imaginary parts take the default values for the double type, which is zero.

The following code shows the structure's explicitly defined constructors:

// Constructors.
public ComplexNumber(double re, double im)
{
Re = re;
Im = im;
}
public ComplexNumber(double re)
{
Re = re;
Im = 0;
}

These constructors initialize the number's real and imaginary parts. If you use the second constructor, which omits the imaginary part, the code sets the imaginary part to 0.

All structures always have an implicit, parameterless constructor that takes no parameters and that leaves the properties at their default values. This means that you cannot create your own parameterless constructor for a structure.

Not having a parameterless constructor is okay in this example, but it could be an issue in other cases where you need that constructor to perform more complex actions. For example, suppose you want to make a Circle structure that represents a circle drawn on a PictureBox. You might want the constructors to give a new Circle an ID number. Because you cannot define a parameterless constructor for a structure, you can't do that.

Possibly worse, you cannot prevent the program from creating a new structure without using a constructor. For example, you might want to let the program draw a maximum of 10 circles. Because you can't create a paremeterless constructor, you can't prevent the program from creating as many Circle structures as it likes.

Finally, because you can't define a parameterless constructor, you cannot set a breakpoint in it.

If you need a parameterless constructor for any of those reasons, then you need to use a class instead of a structure.

Ideally, we could make another constructor that initializes a number from its polar coordinate representation. Unfortunately, that version would take two double values as parameters, so it would have the same signature as the first constructor in the preceding code, and C# would be unable to tell which version you were trying to use.

We cannot create a constructor that uses polar coordinates, but we can create the following factory method instead:

// Polar factory method.
public static ComplexNumber FromPolar(double magnitude, double angle)
{
return new ComplexNumber(
magnitude * Math.Cos(angle),
magnitude * Math.Sin(angle));
}

A factory method creates a new instance of a structure or class. In general, a factory method can perform a lot of work getting the new object ready. For example, it could load data from a database or network, or perform complex validations on its parameters. This example simply uses the number's polar representation to calculate its real and imaginary parts and then uses them to create a new ComplexNumber.

The following code shows static read-only properties that return ComplexNumber objects representing the special values 0, 1, and i:

// Return 0, 1, or i.
private static ComplexNumber ComplexZero = new ComplexNumber();
private static ComplexNumber ComplexOne = new ComplexNumber(1);
private static ComplexNumber ComplexI = new ComplexNumber(0, 1);
public static ComplexNumber Zero
{
get { return ComplexZero; }
}
public static ComplexNumber One
{
get { return ComplexOne; }
}
public static ComplexNumber I
{
get { return ComplexI; }
}

These read-only properties return static instances of ComplexNumber structures representing 0, 1, and i. Because the class is immutable, we don't need to worry about the program modifying these values after the properties return them. This means that it's safe to make these values static so that they are shared by any pieces of code that need to use them.

If the values were not immutable, then the program could modify these shared values. In that case, the static value, ComplexZero, for example, would no longer represent the number 0 + 0i and that could cause problems in other parts of the program. Therefore, if the items are not immutable, you should not return static values. Instead, you should return new objects for this kind of special value.

The following code shows the structure's ToString method:

// Display as in x + yi.
public override string ToString()
{
return $"{Re} + {Im}i";
}

This method simply returns the number's real and imaginary parts in a string with the format x + yi.

When you override the ToString method, other controls can use that method to display a meaningful representation of the object. For example, a ListBox or ComboBox control uses ToString to display the items it contains. Similarly, the Immediate Window, Console window, code editor tooltips, and other Visual Studio features use ToString to display an object's value.

The following code shows the structure's Parse method:

// Parse from a string.
public static ComplexNumber Parse(string s)
{
double re = 0, im = 0;
if (s.Contains("+"))
{
// Real and imaginary parts.
int pos = s.IndexOf("+");
string rePart = s.Substring(0, pos - 1);
re = double.Parse(rePart);

string imPart =
s.Substring(pos + 1).ToLower().Replace("i",
"");
im = double.Parse(imPart);
}
else if (s.ToLower().Contains("i"))
{
// Imaginary part only.
string imPart = s.ToLower().Replace("i", "");
im = double.Parse(imPart);
}
else
{
// Real part only.
re = double.Parse(s);
}

return new ComplexNumber(re, im);
}

The Parse method looks for the + character to determine whether the string contains both real and imaginary parts. If the string contains the + character, the method separates the real and imaginary parts, parses them as double values, and saves them in the local variables rePart and imPart.

If the string does not contain +, then it contains either a real part or an imaginary part, but not both. If the string contains an i, then it contains an imaginary part. The code removes the i, parses the result, and saves it in the imPart variable.

If the string contains neither + nor i, then it contains only a real part. The code parses the string as a double and saves it in the rePart variable.

After it has the parsed the number's real and imaginary parts, the method uses them to create a new ComplexNumber and returns it.

This simple version of the Parse method can only understand the + sign when it is used to separate the real and imaginary parts. It cannot handle more complicated strings such as +1.2E+4 + 5.6E+7 i.

 

This method also handles negative imaginary parts rather awkwardly, as in 7 + -3i. You can try to improve this method if you like.

The following code shows how the structure implements the IEquatable interface and supports other equality tests:

// Equality and IEquatable<ComplexNumber>.
public bool Equals(ComplexNumber other)
{
double dRe = Re - other.Re;
double dIm = Im - other.Im;
return (dRe * dRe + dIm * dIm == 0);
}
public override bool Equals(object obj)
{
if (!(obj is ComplexNumber)) return false;
ComplexNumber other = (ComplexNumber)obj;
return (Re == other.Re) && (Im == other.Im);
}
public bool Equals(ComplexNumber other, double precision)
{
double dRe = Re - other.Re;
double dIm = Im - other.Im;
return (dRe * dRe + dIm * dIm <= precision * precision);
}

The first Equals method satisfies the IEquality interface. It compares two ComplexNumber structures to see if their real and imaginary parts are the same.

The second version overrides the default implementation of Equals to perform the same test with a generic object instead of a ComplexNumber.

As is true with all floating point data types, there may be times when two ComplexNumber values should be equal, but they differ slightly due to rounding errors. The final version of the Equals method returns true if two ComplexNumber values are within a certain distance of each other. For example, the value1.Equals(value2, 0.01) statement will return true if the values are within 0.01 of each other.

Note that this method calculates the distance between two values in the complex plane rather than by simply comparing the values' real and imaginary parts.

The following code show how the structure overloads the == and != operators:

// Comparison operators.
public static bool operator ==(ComplexNumber c1, ComplexNumber c2)
{
return c1.Equals(c2);
}
public static bool operator !=(ComplexNumber c1, ComplexNumber c2)
{
return !(c1 == c2);
}

The == operator simply invokes the Equals method. The != operator invokes == and negates the result.

The == and != operators come as a pair. If you overload one, then you must overload the other.

If you override the Equals method (the second version), then Microsoft recommends that you also override the GetHashCode method. That method returns a hash code that objects such as dictionaries can use to quickly determine whether two objects are different. Because hash codes map complicated objects to comparatively simple codes, there will be cases where two different objects may have the same hash code. However, if two objects have different hash codes, then they are definitely not equal.

Being immutable is an advantage when it comes to hash codes because it means that an object's hash code can never change. In turn, this means that you can add a ComplexNumber to a dictionary and you don't need to worry about its values changing and preventing the dictionary from correctly finding it later.

The following code shows the ComplexNumber structure's GetHashCode method:

// GetHashCode.
public override int GetHashCode()
{
return Re.GetHashCode() ^ (Re + Im).GetHashCode();
}

This method takes the number's real component and invokes its default GetHashCode method. It then adds the number's real and imaginary parts together and invokes the GetHashCode method for the sum. Finally, it uses the bitwise XOR operator to combine the two hash codes and produce its final result.

The reason GetHashCode doesn't simply calculate the hash codes for the real and imaginary parts and combine them is that the calculation would return the same hash code for the values a + b i and b + a i, and it seems somewhat plausible that an application might use two complex numbers having that relationship. The method shown here maps those values to different hash codes so that objects such as dictionaries can handle them more efficiently.

Of course, there are still many more possible ComplexNumber values than there are hash codes (which must fit in an int), so some collisions are unavoidable, but this change makes those collisions occur randomly. If you use the simpler version of GetHashCode and an application happens to use numbers of the form a + b i and b + a i, then collisions would be guaranteed.

The following read-only property returns a complex number's magnitude, which is also sometimes called its modulus or norm:

// Return the number's magnitude. (Also called its modulus or norm.)
public double Magnitude
{
get { return Math.Sqrt(Re * Re + Im * Im); }
}

This property simply calculates the distance from the value to the origin on the complex plane.

The following read-only property returns the number's angle, which is also sometimes called its argument or phase:

// Return the number's angle. (Also called its argument or phase.)
public double Angle
{
get { return Math.Atan2(Im, Re); }
}

This property uses the Atan2 method to calculate the arctangent of the number's imaginary part divided by its real part.

The Angle property’s result is in radians. If you want the angle in degrees, multiply by 180/Math.Pi.

The following conversion operator converts a double into a ComplexNumber:

// Convert the double value re into the ComplexNumber re + 0i.
public static implicit operator ComplexNumber(double re)
{
return new ComplexNumber(re);
}

The real number, R, is the same as the complex number R + 0i, so this operator simply creates and returns the appropriate ComplexNumber. Because this operation does not lose any data, this operator is declared implicit, so the program can use it without a cast operator. For example, the following statement creates a new ComplexNumber with the value 13 + 0i:

ComplexNumber f = 13;

The following operator converts a ComplexNumber into a double:

// Convert the ComplexNumber into a double by dropping 
// the complex part.
public static explicit operator double(ComplexNumber c)
{
return c.Re;
}

This operator drops the number's complex component. Because that causes a loss of data, the method is declared explicit to require the code to use a cast operator to perform the conversion. This prevents you from losing data accidentally. For example, the following statement converts the ComplexNumber f into the double g:

double g = (double)f;

Providing arithmetic operators for the ComplexNumber structure may seem like a daunting task. You would need to write methods to add, subtract, multiply, divide, and negate ComplexNumber values. Then, you would need to write methods showing how to perform those same operations for each of the numeric types byte, sbyte, short, ushort, int, uint, long, ulong, float, and double.

To further complicate matters, you would need to specify each method twice, once for the ComplexNumber on the left and one for the ComplexNumber on the right. For example, you would need methods showing how to calculate int + ComplexNumber and ComplexNumber + int. Mathematically those values are the same when you work with complex numbers, but C# doesn't know that. In general, operators could give different results when the operands are in different orders, or one of the orders might not be defined for a particular order.

Adding all of the combinations of operations, data types, and left/right orderings gives you more than 100 methods that you might need to write!

Fortunately, C# does something that greatly simplifies this problem. When it performs an arithmetic calculation, the program promotes values into the widest data type used by the expression. For example, if you add a long and a double, the long is promoted to a double, the two are added, and the result is a double.

Because we have already defined an implicit conversion operator that converts from a double to a ComplexNumber, C# will automatically make that conversion if necessary. This means that we only need to provide arithmetic methods to work with ComplexNumber values, and C# will automatically promote other values if necessary to perform the calculations. For example, suppose you add a long and a ComplexNumber. The program will promote the long into a double, then convert the double into a ComplexNumber, and finally perform the addition.

The following code shows the ComplexNumber structure's arithmetic operators:

// Arithmetic operators.
public static ComplexNumber operator +(ComplexNumber c1,
ComplexNumber c2)
{
return new ComplexNumber(c1.Re + c2.Re, c1.Im + c2.Im);
}
public static ComplexNumber operator -(ComplexNumber c1)
{
return new ComplexNumber(-c1.Re, -c1.Im);
}
public static ComplexNumber operator -(ComplexNumber c1,
ComplexNumber c2)
{
return new ComplexNumber(c1.Re - c2.Re, c1.Im - c2.Im);
}
public static ComplexNumber operator *(ComplexNumber c1,
ComplexNumber c2)
{
return new ComplexNumber(
c1.Re * c2.Re - c1.Im * c2.Im,
c1.Re * c2.Im + c1.Im * c2.Re);
}
public static ComplexNumber operator /(ComplexNumber c1,
ComplexNumber c2)
{
double denominator = c2.Re * c2.Re + c2.Im * c2.Im;
return new ComplexNumber(
(c1.Re * c2.Re + c1.Im * c2.Im) / denominator,
(c1.Im * c2.Re - c1.Re * c2.Im) / denominator);
}

These methods are straightforward, so I won't describe them in detail.

The ComplexNumbers example solution tests the ComplexNumber class. The code is long but straightforward, so I won't describe it in detail here. It tests a number of ComplexNumber features, including different constructors, the static Zero, One, and I properties, ToString, various arithmetic operations with ComplexNumber and integer values, GetHashCode, and more.

Download the ComplexNumbers example solution to see the results and to see additional details.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.32.76