Complex numbers, such as 2 + 5i where i = sqrt(-1), have extensive applications in math and physics. So it deserves an another look itself. In this note, I'll go through the decisions I make in the design/modeling. These decisions, I believe, not only work in this case, but also in other common cases, such as rational numbers p / q where p and q are integers, money(cash, with amount and currency), etc. The code is in C#, while I briefly mention Java as well.
In C#, for performance reason, I model it as a struct because struct data is allocated on the stack, not on the heap.
struct ComplexNumber
{
double real;
double imag;
public ComplexNumber(double real, double imag)
{
this.real = real;
this.imag = imag;
}
}
Here I use the type of double for both real and imaginary parts. I don't find applications that warrants other types, such as float, long, or int. However, with a little bit imagination, we should switch the types if the application needs, such as the following case,
- if memory is tight and float can do the job.
- if integer(int or long) is good enough to do the job, because the arithmetics of integers are faster.
Though we could utilize generics to parametize the types of real and imaginary parts, I feel this decision is more close to the general definition of complex numbers.
Another consideration is the copy constructor, do we need it? My preference is not having it, because there are only two fields and it's clearer when we explicitly initiate a complex number with its real and imaginary parts.
Since we model complex numbers as value objects, namely two complex numbers are equal if their real parts are equal and imaginary parts are equal. So we override the Equals() and GetHashCode() methods
public override bool Equals(object obj)
{
if (obj == null) return false;
if (!(obj is ComplexNumber)) return false;
ComplexNumber c = (ComplexNumber)obj;
return real == c.real && imag == c.imag;
}
public override int GetHashCode()
{
return real.GetHashCode() + 19 * imag.GetHashCode();
}
Here 19 is just a prime number arbitrarily picked to generate not-so-often-clashed hash code. For convenience, we override ToString() as well.
public override string ToString()
{
if (imag == 0) return "" + real;
else if (imag > 0) return "" + real + "+" + imag + "i";
else return "" + real + imag + "i";
}
There are a few basic operations for complex numbers, norm(), arg(), conjugate(). And for convenience, I provide an extra method, reciprocal()
public double norm()
{
return Math.Sqrt(real * real + imag * imag);
}
public double arg()
{
if (real == 0)
{
if (imag == 0) return 0;
if (imag > 0) return Math.PI / 2;
else return -Math.PI / 2;
}
return Math.Atan(imag / real);
}
public ComplexNumber conjugate()
{
return new ComplexNumber(real, -imag);
}
public ComplexNumber reciprocal()
{
double r2 = real * real + imag * imag;
if (r2 == 0) // should we check precision
throw new DivideByZeroException("there is no reciprocal for zero!");
return new ComplexNumber(real / r2, -imag / r2);
}
Next, since C# has operator overloading, so I add them here
public static ComplexNumber operator +(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
return new ComplexNumber(complexNumber1.real + complexNumber2.real,
complexNumber1.imag + complexNumber2.imag);
}
public static ComplexNumber operator -(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
return new ComplexNumber(complexNumber1.real - complexNumber2.real,
complexNumber1.imag - complexNumber2.imag);
}
public static ComplexNumber operator *(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
double r = complexNumber1.real * complexNumber2.real -
complexNumber1.imag * complexNumber2.imag;
double i = complexNumber1.imag * complexNumber2.real -
complexNumber1.real * complexNumber2.imag;
return new ComplexNumber(r, i);
}
public static ComplexNumber operator /(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
double r2 = complexNumber2.real * complexNumber2.real +
complexNumber2.imag * complexNumber2.imag;
if (r2 == 0) // should we check precision
throw new DivideByZeroException("divided by zero!");
double r = complexNumber1.real * complexNumber2.real +
complexNumber1.imag * complexNumber2.imag;
double i = complexNumber1.imag * complexNumber2.real -
complexNumber1.real * complexNumber2.imag;
return new ComplexNumber(r / r2, i / r2);
}
And we shouldn't forget the following since we override the Equals() method
public static bool operator ==(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
// we need cast this to object, otherwise this == is a recursive call and end up an infinite loop
if ((object)complexNumber1 == null) return (object)complexNumber2 == null;
return complexNumber1.Equals(complexNumber2);
}
public static bool operator !=(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
// we need cast this to object, otherwise this == is a recursive call and end up an infinite loop
if ((object)complexNumber1 == null) return (object)complexNumber2 != null;
return !complexNumber1.Equals(complexNumber2);
}
So far so good, we can write a testcase, I just use console printout.
ComplexNumber a = new ComplexNumber(5, 12);
ComplexNumber b = new ComplexNumber(9, 11);
ComplexNumber c = new ComplexNumber(0, 0);
ComplexNumber d = new ComplexNumber(5, 12);
// test equalities
Console.WriteLine("a is not null: " + (a.Equals(null)) + ", " + (a == null));
Console.WriteLine("a is not a string: " + (a.Equals("abc")));
Console.WriteLine("a is not a real number: " + (a.Equals(2)));
Console.WriteLine("value object equal: " + (a.Equals(d)) + ", " + (a == d));
Console.WriteLine("value object not equal: " + (a == b));
// test behaviors
Console.WriteLine("a=" + a);
Console.WriteLine("a.norm=" + a.norm());
Console.WriteLine("a.arg=" + a.arg());
Console.WriteLine("a.conj=" + a.conjugate());
Console.WriteLine("a.reci=" + a.reciprocal());
try { c.reciprocal(); }
catch (DivideByZeroException dze)
{
Console.WriteLine("divided by zero: " + dze.Message);
}
Now the question arise, what if we have
a + 6
we are adding a complex number to a double. So we need something extra. There are two ways to accomplish this. The first way is to add an implicit operator
public static implicit operator ComplexNumber(double d)
{
return new ComplexNumber(d, 0);
}
This will convert a double to a ComplexNumber in the arithmetic operations. The second way is to add explicitly code the methods, for instance,
public static ComplexNumber operator +(ComplexNumber complexNumber, double real)
{
return new ComplexNumber(complexNumber.real + real, complexNumber.imag);
}
public static ComplexNumber operator +(double real, ComplexNumber complexNumber)
{
return new ComplexNumber(complexNumber.real + real, complexNumber.imag);
}
This method is more straight forward and thus faster (without creating extra ComplexNumber object). We should do this for all 4 arithmetic operations.
Now let's go back to implicit conversion(i.e., the first method), do we really need this method? The only usage of this method is the case where we have something special in complex number context but not in the real(double) context. From my experience I know this is indeed the case, so we keep this method. In addition to implicit conversion, we have explicit conversion too(namely, cast). And there are two directions in each conversion, so totally we have 4 cases:
- implicit operator ComplexNumber(double d)
- implicit operator double(ComplexNumber c)
- explicit operator ComplexNumber(double d)
- explicit operator double(ComplexNumber c)
First we can have 1, 3 at the same time, or 2, 4 at the same time(complier error). Next, do we really want to convert a ComplexNumber object to a double? I saw some code around that returns the real part when converting, it is not the case in general. So I classify this as an invalid case and rule out 2, 4. Now the question is whether we want it to be explicit or implicit. Since there is no point to do explicitly, such as (ComplexNumber)d, we just keep the implicit version.
Again, we test it
Console.WriteLine("explicit cast:" + ((ComplexNumber)3.0));
Console.WriteLine("neg=" + (-a));
Console.WriteLine("sum=" + (a + b));
Console.WriteLine("diff=" + (a - b));
Console.WriteLine("prod=" + (a * b));
Console.WriteLine("divi=" + (a / b));
Console.WriteLine("add=" + (a + 3.5));
Console.WriteLine("add=" + (3.5 + a));
Console.WriteLine("minus=" + (a - 3.5));
Console.WriteLine("minus=" + (3.5 - a));
Console.WriteLine("multi=" + (a * 2));
Console.WriteLine("multi=" + (2 * a));
Console.WriteLine("div=" + (a / 2));
Console.WriteLine("div=" + (2 / a));
At this point, we are done with the ComplexNumber struct. Let's now see how useful this is.
The first case we are interested in is the array of ComplexNumber. Our testing shows this approach is not performant well. Here is the testing code.
// initialize data
ComplexNumber[] cnums = new ComplexNumber[1000000];
double[] re = new double[1000000];
double[] im = new double[1000000];
for (int i = 0; i < cnums.Length; i++)
{
cnums[i] = new ComplexNumber(i + 3, i + 7);
re[i] = i + 3;
im[i] = i + 7;
}
ComplexNumber sum = new ComplexNumber(0, 0);
DateTime startTime = DateTime.Now;
for (int i = 0; i < cnums.Length; i++)
{
sum += cnums[i];
}
DateTime stopTime = DateTime.Now;
TimeSpan duration = stopTime - startTime;
Console.WriteLine(duration);
double resum = 0, imsum = 0;
startTime = DateTime.Now;
for (int i = 0; i < cnums.Length; i++)
{
resum += re[i];
imsum += im[i];
}
stopTime = DateTime.Now;
duration = stopTime - startTime;
Console.WriteLine(duration);
We initialize a million complex numbers and then take the sum. We compare this method with the approach where we use two double arrays. The result is this:
00:00:00.0312502
00:00:00.0156251
The difference is 100%. Though individual object is working fine, the composition of an array of the same struct works poorly when the size of the array is large.
The second case is matrix of complex numbers. Ideally we want to have something like this:
Matrix<double> md = new Matrix<double>(100, 200);
Matrix<ComplexNumber> mc = new Matrix<ComplexNumber>(100, 200);
and then carry out whatever operation we need, such as LU decomposition. Since the LU decomposition is the same for both real number and complex number, we would like to write one piece of code for the decomposition, no matter what the underlying number system is. However, in C#, there is no super class/struct/interface for operators(+, -, *, /), we can't code something like this
public class Matrix<T> { ... }
to work with all number systems.
Here we show two cases where C# (as well as Java) and other strong types languages don't work well with numerical computing which demands high performance. C++ can work out through weak typed templates, but the error message is very ugly to digest if we pass in a wrong object.
The lack of support from the language level suggests that it is not appropriate to abstract the details at the number system level. Instead, we should abstract at a higher level in the application (e.g., at the array of complex numbers in the first case).
In the object oriented world, we do have a lot of cases where we need a lot of small objects, especially when we build a foundation for a certain application, one good book is Analysis Patterns written by Martin Fowler. Efficient composition of these small objects is crucial to successful usage of an object oriented language.
A side note is that once we create the new complex number, we need to create corresponding functions, such as complex versions of sin, cos, etc. Though I don't foresee any problem with it, it's pretty tedious. By the way, the way that Java and C# build Math libs is not appropriate massive composition, but this is a different topics.
分享到:
相关推荐
Lesson 1 On the Farm练习题及答案.doc
Lesson 6 Number 6-10练习题及答案.doc
这篇文档是关于冀教版小学三年级英语上册Lesson 4 "Number 1-5"的教案,主要目标是教授学生英语数字1至5以及相关的句型。以下是详细的知识点: 1. **知识目标**: - 单词学习:本节课的重点是让学生能够听、说、...
Lesson 01-lesson 02 创建一个OpenGL窗口: 如何创建三角形和四边形 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53679 Lesson 03-lesson 04 添加颜色 旋转 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53682 ...
(完整版)三年级英语上册lesson4Number1-5教案(新版)冀教版.pdf
Lesson 01-lesson 02 创建一个OpenGL窗口: 如何创建三角形和四边形 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53679 Lesson 03-lesson 04 添加颜色 旋转 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53682 ...
Unit 1 Lesson 4 On a School Day课堂练习(冀教版八上)2.doc
Lesson 01-lesson 02 创建一个OpenGL窗口: 如何创建三角形和四边形 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53679 Lesson 03-lesson 04 添加颜色 旋转 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53682 ...
Lesson 01-lesson 02 创建一个OpenGL窗口: 如何创建三角形和四边形 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53679 Lesson 03-lesson 04 添加颜色 旋转 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53682 ...
Lesson 01-lesson 02 创建一个OpenGL窗口: 如何创建三角形和四边形 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53679 Lesson 03-lesson 04 添加颜色 旋转 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53682 ...
《深入浅出Spring框架:基于Java的lesson4.1项目解析》 在Java开发领域,Spring框架无疑是最为广泛使用的轻量级应用框架之一。它以其强大的功能、灵活的配置和丰富的生态,深受开发者喜爱。本篇文章将围绕“lesson...
在Lesson2中,你会学习到`var`、`let`和`const`的区别,以及基本数据类型(如`String`、`Number`、`Boolean`、`Null`、`Undefined`)和复合数据类型(如`Object`和`Array`)的使用。 2. **操作符与表达式** 课程会...
这篇文档是关于小学英语三年级下册接力版Lesson 5的教学设计,主题是"Who is on the left of Mary?"。这个教学设计旨在帮助学生掌握与左右位置相关的英语句型,包括"Who is on the left/right of...?"以及"...is on ...
四年级英语上册 Lesson2 Whats on the desk第二课时教案 科普版.doc
新概念英语第二册Lesson5Lesson6PPT课件.pptx
lesson 1.rarlesson 1.rarlesson 1.rarlesson 1.rarlesson 1.rarlesson 1.rarlesson 1.rarlesson 1.rarlesson 1.rar
Lesson 01-lesson 02 创建一个OpenGL窗口: 如何创建三角形和四边形 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53679 Lesson 03-lesson 04 添加颜色 旋转 http://ieee.org.cn/dispbbs.asp?boardID=61&ID=53682 ...
2015春八年级英语下册 Unit 4 Lesson 22 Travel on the Internet课件 (新版)冀教版
2015春八年级英语下册 Unit 1 lesson 5 Babysitting on a Spring Day课件 (新版)冀教版
这篇文档是关于冀教版小学六年级英语上册《Lesson 16 A Skating Lesson》的教学教案,旨在帮助教师设计并实施一堂生动有趣的英语课堂。教案的核心目标是教授学生三个关键的词汇和短语,以及现在进行时的用法。 教学...