CSharp中的Vector结构

混江龙づ霸主 提交于 2020-01-13 13:57:46

本节将开发一个结构Vector,来演示运算符重载,这个Vector结构表示一个三维矢量。如果数学不是你的强项,不必担心,我们会使这个例子尽可能简单。三维矢量

只是三个(double)数字的一个集合,说明物体和原点之间的距离,表示数字的变量是x、y和z,x表示物体与原点在x方向上的距离,y表示它与原点在y方向上的距离,

z表示高度。把这3个数字组合起来,就得到总距离。例如,如果x=3.0, y=3.0, z=1.0,一般可以写作(3.0, 3.0, 1.0),表示物体与原点在x方向上的距离是3,与原点在

y方向上的距离是3,高度为1。

矢量可以与矢量或数字相加或相乘。在这里我们使用术语"标量"(scalar),它是数字的数学用语-- 在C#中,就是一个double。相加的作用是很明显的。如果先移动(3.0, 3.0, 1.0),再移动(2.0, -4.0, -4.0),总移动量就是把这两个矢量加起来。矢量的相加是指把每个元素分别相加,因此得到(5.0, -1.0,-3.0)。此时,数学表达式总是写成c=a+b,其中a和b是矢量,c是结果矢量。这与使用Vector结构的方式是一样的。

注意:

这个例子是作为一个结构来开发的,而不是类,但这并不重要。运算符重载用于结构和类时,其工作方式是一样的。

下面是Vector的定义-- 包含成员字段、构造函数和一个ToString()重写方法,以便查看Vector的内容,最后是运算符重载:

namespace Wrox.ProCSharp.OOCSharp
{
    struct Vector
    {
        public double x, y, z;
        public Vector(double x, double y, double z)
        {
            this.x = x; this.y = y; this.z = z;
        }
        public Vector(Vector rhs)
        {
            x = rhs.x; y = rhs.y; z = rhs.z;
        }
        public override string ToString()
        {
            return "( " + x + " , " + y + " , " + z + " )";
        }
    }
}

这里提供了两个构造函数,通过传递每个元素的值,或者提供另一个复制其值的Vector,来指定矢量的初始值。第二个构造函数带一个Vector参数,通常称为复制构造函数,因为它们允许通过复制另一个实例来初始化一个类或结构实例。注意,为了简单起见,把字段设置为public。也可以把它们设置为private,编写相应的属性来访问它们,这样做不会改变这个程序的功能,只是代码会复杂一些。

下面是Vector结构的有趣部分-- 为+运算符提供支持的运算符重载:

public static Vector operator + (Vector lhs, Vector rhs)
{
    Vector result = new Vector(lhs);
    result.x += rhs.x;
    result.y += rhs.y;
    result.z += rhs.z;
    return result;
}

运算符重载的声明方式与方法的声明方式相同,但operator关键字告诉编译器,它实际上是一个运算符重载,后面是相关运算符的符号,在本例中就是+。返回类型是在使用这个运算符时获得的类型。在本例中,把两个矢量加起来会得到另一个矢量,所以返回类型就是Vector。对于这个+运算符重载,返回类型与包含类一样,但这种情况并不是必需的。两个参数就是要操作的对象。对于二元运算符(带两个参数),如+和-运算符,第一个参数是放在运算符左边的值,第二个参数是放在运算符右边的值。

注意:

一般把运算符左边的参数命名为lhs,运算符右边的的参数命名为rhs。

C#要求所有的运算符重载都声明为public和static,这表示它们与它们的类或结构相关联,而不是与实例相关联,所以运算符重载的代码体不能访问非静态类成员,也不能访问this标识符;这是可以的,因为参数提供了运算符执行任务所需要知道的所有数据。

前面介绍了声明运算符+的语法,下面看看运算符内部的情况:

{
    Vector result = new Vector(lhs);result.x += rhs.x;
    result.y += rhs.y;
    result.z += rhs.z;
    return result;
}

这部分代码与声明方法的代码是完全相同的,显然,它返回一个矢量,其中包含前面定义的lhs和rhs的和,即把x、y和z分别相加。

下面需要编写一些简单的代码,测试Vector结构:

static void Main()
{
    Vector vect1, vect2, vect3;         
    vect1 = new Vector(3.0, 3.0, 1.0);
    vect2 = new Vector(2.0,-4.0,-4.0);
    vect3 = vect1 + vect2;         
    Console.WriteLine("vect1 = " + vect1.ToString());
    Console.WriteLine("vect2 = " + vect2.ToString());
    Console.WriteLine("vect3 = " + vect3.ToString());
}

 

把这些代码保存为Vectors.cs,编译并运行它,结果如下:

Vectorsvect1 = ( 3 , 3 , 1 )vect2 = ( 2 ,-4 ,-4 )vect3 = ( 5 ,-1 ,-3 )
 

6.4.2  运算符重载的示例:Vector结构

本节将开发一个结构Vector,来演示运算符重载,这个Vector结构表示一个三维矢量。如果数学不是你的强项,不必担心,我们会使这个例子尽可能简单。三维矢量只是三个(double)数字的一个集合,说明物体和原点之间的距离,表示数字的变量是x、y和z,x表示物体与原点在x方向上的距离,y表示它与原点在y方向上的距离,z表示高度。把这3个数字组合起来,就得到总距离。例如,如果x=3.0, y=3.0, z=1.0,一般可以写作(3.0, 3.0, 1.0),表示物体与原点在x方向上的距离是3,与原点在y方向上的距离是3,高度为1。

矢量可以与矢量或数字相加或相乘。在这里我们使用术语“标量”(scalar),它是数字的数学用语—— 在C#中,就是一个double。相加的作用是很明显的。如果先移动(3.0, 3.0, 1.0),再移动(2.0, –4.0, –4.0),总移动量就是把这两个矢量加起来。矢量的相加是指把每个元素分别相加,因此得到(6.0, –1.0,–3.0)。此时,数学表达式总是写成c=a+b,其中a和b是矢量,c是结果矢量。这与使用Vector结构的方式是一样的。

注意:
这个例子是作为一个结构来开发的,而不是类,但这并不重要。运算符重载用于结构和类时,其工作方式是一样的。

下面是Vector的定义—— 包含成员字段、构造函数和一个ToString()重写方法,以便查看Vector的内容,最后是运算符重载:

namespace Wrox.ProCSharp.OOCSharp
{
   struct Vector
   {
      public double x, y, z;

      public Vector(double x, double y, double z)
      {
         this.x = x;
         this.y = y;
         this.z = z;
      }

      public Vector(Vector rhs)
      {
         x = rhs.x;
         y = rhs.y;
         z = rhs.z;
      }

      public override string ToString()
      {
         return "( " + x + " , " + y + " , " + z + " )"; 
      }
//这里提供了两个构造函数,通过传递每个元素的值,或者提供另一个复制其值的Vector,来指定矢量的初始值。
//第二个构造函数带一个Vector参数,通常称为复制构造函数,因为它们允许通过复制另一个实例来初始化一个类或结构实例。
//注意,为了简单起见,把字段设置为public。也可以把它们设置为private,编写相应的属性来访问它们,这样做不会改变这个程序的功能,
//只是代码会复杂一些。下面是Vector结构的有趣部分—— 为+运算符提供支持的运算符重载:
      public static Vector operator + (Vector lhs, Vector rhs)
      {
         Vector result = new Vector(lhs);
         result.x += rhs.x;
         result.y += rhs.y;
         result.z += rhs.z;
         return result;
      }
   }
}

运算符重载的声明方

 

式与方法的声明方式相同,但operator关键字告诉编译器,它实际上是一个运算符重载,后面是相关运算符的符号,在本例中就是+。返回类型是在使用这个运算符时获得的类型。在本例中,把两个矢量加起来会得到另一个矢量,所以返回类型就是Vector。对于这个+运算符重载,返回类型与包含类一样,但这种情况并不是必需的。两个参数就是要操作的对象。对于二元运算符(带两个参数),如+和-运算符,第一个参数是放在运算符左边的值,第二个参数是放在运算符右边的值。

C#要求所有的运算符重载都声明为public和static,这表示它们与它们的类或结构相关联,而不是与实例相关联,所以运算符重载的代码体不能访问非静态类成员,也不能访问this标识符;这是可以的,因为参数提供了运算符执行任务所需要知道的所有数据。

前面介绍了声明运算符+的语法,下面看看运算符内部的情况:

      {
         Vector result = new Vector(lhs);
         result.x += rhs.x;
         result.y += rhs.y;
         result.z += rhs.z;
         return result;
      }

这部分代码与声明方法的代码是完全相同的,显然,它返回一个矢量,其中包含前面定义的lhs和rhs的和,即把x、y和z分别相加。

下面需要编写一些简单的代码,测试Vector结构:

      static void Main()
      {
         Vector vect1, vect2, vect3;

         vect1 = new Vector(3.0, 3.0, 1.0);
         vect2 = new Vector(2.0,¬¬¬–4.0,–4.0);
         vect3 = vect1 + vect2;

         Console.WriteLine("vect1 = " + vect1.ToString());
         Console.WriteLine("vect2 = " + vect2.ToString());
         Console.WriteLine("vect3 = " + vect3.ToString());
      }

把这些代码保存为Vectors.cs,编译并运行它,结果如下:

Vectors
vect1 = ( 3 , 3 , 1 )
vect2 = ( 2 ,–4 ,–4 )
vect3 = ( 5 ,–1 ,–3 )

1. 添加更多的重载

矢量除了可以相加之外,还可以相乘、相减,比较它们的值。本节通过添加几个运算符重载,扩展了这个例子。这并不是一个功能全面的真实的Vector类型,但足以说明运算符重载的其他方面了。首先要重载乘法运算符,以支持标量和矢量的相乘以及矢量和矢量的相乘。

矢量乘以标量只是矢量的元素分别与标量相乘,例如,2 * (1.0, 2.5, 2.0)就等于(2.0, 5.0, 4.0)。相关的运算符重载如下所示。

      public static Vector operator * (double lhs, Vector rhs)
      {
         return new Vector(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);
      }

但这还不够,如果a和b声明为Vector 类型,就可以编写下面的代码:

b = 2 * a;

编译器会隐式地把整数2转换为double类型,以匹配运算符重载的签名。但不能编译下面的代码:

b = a * 2;

编译器处理运算符重载的方式和处理方法重载的方式是一样的。它会查看给定运算符的所有可用重载,找到与之最匹配的那个运算符重载。上面的语句要求第一个参数是Vector,第二个参数是整数,或者可以隐式转换为整数的其他数据类型。我们没有提供这样一个重载。有一个运算符重载,其参数是一个double和一个Vector,但编译器不能改变参数的顺序,所以这是不行的。还需要显式定义一个运算符重载,其参数是一个Vector和一个double,有两种方式可以定义这样一个运算符重载,第一种方式和处理所有运算符的方式一样,显式执行矢量相乘操作:

      public static Vector operator * (Vector lhs, double rhs)
      {
         return new Vector(rhs * lhs.x, rhs * lhs.y, rhs *lhs.z);
      }

假定已经编写了执行相乘操作的代码,最好重复使用该代码:

      public static Vector operator * (Vector lhs, double rhs)
      {
         return rhs * lhs;
      }

这段代码会告诉编译器,如果有Vector和double的相乘操作,编译器就使参数的顺序反序,调用另一个运算符重载。在本章的示例代码中,我们使用第二个版本,它看起来比较简洁。利用这个版本可以编写出维护性更好的代码,因为不需要复制代码,就可在两个独立的重载中执行相乘操作。

下一个要重载的运算符是矢量相乘。在数学上,矢量相乘有两种方式,但这里我们感兴趣的是点积或内积,其结果实际上是一个标量。这就是我们介绍这个例子的原因,所以算术运算符不必返回与定义它们的类相同的类型。

在数学上,如果有两个矢量(x, y, z)和(X, Y, Z),其内积就是x*X + y*Y + z*Z的值。两个矢量这样相乘是很奇怪的,但这是很有效的,因为它可以用于计算各种其他的数。当然,如果要使用Direct3D 或DirectDraw编写代码来显示复杂的3D图形,在计算对象放在屏幕上的什么位置时,常常需要编写代码来计算矢量的内积,作为中间步骤。这里我们关心的是使用Vector编写出double X = a*b,其中a和b是矢量,并计算出它们的点积。相关的运算符重载如下所示:

      public static double operator * (Vector lhs, Vector rhs)
      {
         return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
      }

定义了算术运算符后,就可以用一个简单的测试方法来看看它们是否能正常运行:

static void Main()
{
   // stuff to demonstrate arithmetic operations
   Vector vect1, vect2, vect3;
   vect1 = new Vector(1.0, 1.5, 2.0);
   vect2 = new Vector(0.0, 0.0,–10.0);

   vect3 = vect1 + vect2;

   Console.WriteLine("vect1 = " + vect1);
   Console.WriteLine("vect2 = " + vect2);
   Console.WriteLine("vect3 = vect1 + vect2 = " + vect3);
   Console.WriteLine("2*vect3 = " + 2*vect3);

   vect3 += vect2;

   Console.WriteLine("vect3+=vect2 gives " + vect3);

   vect3 = vect1*2;

   Console.WriteLine("Setting vect3=vect1*2 gives " + vect3);

   double dot = vect1*vect3;

   Console.WriteLine("vect1*vect3 = " + dot);
}

运行代码(Vectors2.cs),得到如下所示的结果:

Vectors2
vect1 = ( 1 , 1.5 , 2 )
vect2 = ( 0 , 0 ,–10 )
vect3 = vect1 + vect2 = ( 1 , 1.5 ,–8 )
2*vect3 = ( 2 , 3 ,–16 )
vect3+=vect2 gives ( 1 , 1.5 ,–18 )
Setting vect3=vect1*2 gives ( 2 , 3 , 4 )
vect1*vect3 = 14.5

这说明,运算符重载会给出正确的结果,但如果仔细看看测试代码,就会惊奇地注意到,实际上我们使用的是没有重载的运算符—— 相加赋值运算符+=:

vect3 += vect2;
Console.WriteLine("vect3 += vect2 gives " + vect3);

虽然+=一般用作单个运算符,但实际上其操作分为两部分:相加和赋值。与C++不同,C#不允许重载=运算符,但如果重载+运算符,编译器就会自动使用+运算符的重载来执行+=运算符的操作。–=、&=、*=和/=赋值运算符也遵循此规则。

2. 比较运算符的重载

C#中有6个比较运算符,它们分为3对:

● == 和 !=
● > 和 <
● >= 和 <=

C#要求成对重载比较运算符。如果重载了==,也必须重载!=,否则会产生编译错误。另外,比较运算符必须返回bool类型的值。这是它们与算术运算符的根本区别。两个数相加或相减的结果,理论上取决于数的类型。而两个Vector的相乘会得到一个标量。另一个例子是.NET基类System.DateTime,两个DateTime实例相减,得到的结果不是DateTime,而是一个System.TimeSpan实例,但比较运算得到的如果不是bool类型的值,就没有任何意义。

注意:
在重载==和!=时,还应重载从System.Object中继承的Equals()和GetHashCode()方法,否则会产生一个编译警告。原因是Equals()方法应执行与==运算符相同的相等逻辑。

除了这些区别外,重载比较运算符所遵循的规则与算术运算符相同。但比较两个数并不像想象的那么简单,例如,如果比较两个对象引用,就是比较存储对象的内存地址。比较运算符很少进行这样的比较,所以必须编写运算符,比较对象的值,返回相应的布尔结果。下面给Vector结构重载==和!=运算符。首先是==的执行代码:

      public static bool operator = = (Vector lhs, Vector rhs)  
      {
         if (lhs.x = = rhs.x && lhs.y = = rhs.y && lhs.z = = rhs.z)
            return true;
         else
            return false;
      }

这种方式仅根据矢量组成部分的值,来对它们进行相等比较。对于大多数结构,这就是我们希望的,但在某些情况下,可能需要仔细考虑相等的含义,例如,如果有嵌入的类,是应比较引用是否指向同一个对象(浅度比较),还是应比较对象的值是否相等(深度比较)?

注意:
不要通过调用从System.Object中继承的Equals()方法的实例版本,来重载比较运算符,如果这么做,在objA是null时计算(objA==objB),就会产生一个异常,因为.NET运行库会试图计算null.Equals(objB)。采用其他方法(重写Equals()方法,调用比较运算符)比较安全。

还需要重载运算符!=,采用的方式如下:

      public static bool operator != (Vector lhs, Vector rhs)
      {
      return ! (lhs == rhs);
      }

像往常一样,用一些测试代码检查重写方法的工作情况,这次定义3个Vector对象,并进行比较:

      static void Main()
      {
         Vector vect1, vect2, vect3;

         vect1 = new Vector(3.0, 3.0,–10.0);
         vect2 = new Vector(3.0, 3.0,–10.0);
         vect3 = new Vector(2.0, 3.0, 6.0);

         Console.WriteLine("vect1= =vect2 returns  " + (vect1= =vect2));
         Console.WriteLine("vect1= =vect3 returns  " + (vect1= =vect3));
         Console.WriteLine("vect2= =vect3 returns  " + (vect2= =vect3));

         Console.WriteLine();

         Console.WriteLine("vect1!=vect2 returns  " + (vect1!=vect2));
         Console.WriteLine("vect1!=vect3 returns  " + (vect1!=vect3));
         Console.WriteLine("vect2!=vect3 returns  " + (vect2!=vect3));
      }

编译这些代码(下载代码中的Vectors3.cs),会得到一个编译器警告,因为我们没有为Vector重写Equals(),对于本例,这是不重要的,所以忽略它。

csc Vectors3.cs
Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.42
for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727
Copyright (C) Microsoft Corporation 2001-2006. All rights reserved.
Vectors3.cs(5,11): warning CS0660: 'Wrox.ProCSharp.OOCSharp.Vector' defines
        operator = = or operator != but does not override Object.Equals(object o)
Vectors3.cs(5,11): warning CS0661: 'Wrox.ProCSharp.OOCSharp.Vector' defines
        operator = = or operator != but does not override Object.GetHashCode()

在命令行上运行该示例,生成如下结果:

Vectors3
vect1= =vect2  returns  True
vect1= =vect3  returns  False
vect2= =vect3  returns  False
vect1!=vect2   returns  False
vect1!=vect3   returns  True
vect2!=vect3   returns  True

3.  可以重载的运算符

并不是所有的运算符都可以重载。可以重载的运算符如表6-5所示。

表  6-5

类 别 运 算 符 限 制
算术二元运算符 +, *, /, –, %
算术一元运算符 +, –, ++, ––
按位二元运算符 &, |, ^, <<, >>
按位一元运算符 !, ~, true, false true和false运算符必须成对重载
比较运算符 ==, !=, >=, <, <=, > 必须成对重载
赋值运算符 +=,–=,*=,/=,>>=,<<=,%=,&=,|=,^= 不能显式重载这些运算符,在重写单个运算符如+,–,%等时,它们会被隐式重写
索引运算符 []

不能直接重载索引运算符。第2章介绍的索引器成员类型允许在类和结构上支持索引运算符

数据类型转换运算符 ()

不能直接重载数据类型转换运算符。用户定义的数据类型转换(本章的后面介绍)允许定义定制的数据类型转换

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!