C#高编 - 运算符和类型强制转换

摘要

  • C#中的运算符
  • 处理引用类型和值类型时相等的含义
  • 基本数据类型之间的数据转换
  • 使用装箱技术把值类型转换为引用类型
  • 通过类型强制转换在引用类型之间转换
  • 重载标准的运算符以支持自定义类型
  • 给自定义类型添加类型强制转换运算符

1.运算符

算数运算符:+ - * / %

逻辑运算符:& | ^ ~ && || !

字符串连接运算符:+

增量和减量运算符:++ --

移位运算符:<< >>

比较运算符:== != <> <= >=

赋值运算符:= += -= *= /= %= &= |= ^= <<= >>=

成员访问运算符:.

索引运算符:[]

类型转换运算符:()

条件运算符(三元运算符):?:

委托连接和删除运算符:+ -

对象创建运算符:new

类型信息运算符:sizeof is typeof as

溢出异常控制运算符:checked unchecked

间接寻址运算符:[]

名称空间别名限定符:::

空合并运算符:??

其中:sizeof、*、->、&:只能用于不安全的代码

2.checked和unchecked运算符

把一个代码块标记为checked,CLR就会执行溢出检查,如果发生溢出,就抛出OverflowException异常。如,

byte b = 255;
checked
{
    b++;
}
Console.WriteLine(b.ToString());

注意:用/checked编译器选项进行编译,就可以检查程序中所有未标记代码中的溢出。

相反,要禁止溢出检查,则用unchecked,不会抛出异常,但会丢失数据,溢出的位会别丢弃。

unchecked是默认行为,当需要在标记为checked的代码块中增加几行未检查的代码时才会使用。

3.is运算符

可以检查对象是否与特定的类型兼容,“兼容”表示对象或者该类型,或者派生自该类型。如,

int i = 10;
if(i is object)
{
    Console.WriteLine("i is an object");
}

4.as运算符

用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功;如果类型不兼容,as运算符就会返回null值。

5.sizeof运算符

确定栈中值类型需要的长度(单位是字节)

对于复杂类型(和非基元类型)使用sizeof运算符,就需要把safe放在unsafe块中,如,

unsafe
{
    Console.WriteLine(sizeof(Customer));
}

6.可空类型和运算符

通常可控类型与一元或二元运算符一起使用时,如果其中一个操作数或两个操作数都是null,其结果就是null。如

int? a = null;
int? b = a+4;//b = null

在比较可空类型时,只要有一个操作数是null,比较结果就是false。即不能因为一个条件是false,就认为条件的对立面是true,这在使用非可空类型的程序中很常见。

7.空合并运算符

提供了一种快捷方式,可以在处理可空类型和引用类型时表示null可能的值。

第一个数必须是一个可空类型或引用类型;第二个数必须与第一个的类型相同,或者可以隐含地转换,

  • 如果第一个操作不是null,整个表达式就等于第一个操作的值。
  • 如果第一个操作数是null,整个表达式就等于第二个操作数的值。

int? a = null;
int b;
b = a ?? 10;//b has the value 10
a = 3;
b = a ?? 10;//b has the value 3

8.类型转换

隐式转换:可以从较小的整数类型隐式转换成较大的整数类型,反之不可以。也可以在整数和浮点数之间转换,但会丢失4个字节的数据。

无符号的变量值可以转换为有符号的变量,只要大小在有符号的变量的范围之内即可。

可空类型转换为其它可空类型,如int?转换为long?,或者非可空类型转换为可空类型,遵循非可空类型的转换规则。

byte value1 = 10;
byte value2 = 20;
long value;
total = value1 + value2;
Console.WriteLine(total);

 

显式转换:

以下是部分不能隐式转换类型的场合,需要进行强制转换,如int i = (int)val;

int>>short、int>>uint、uint>>int、float>>int、任何数字>>char、decimal>>任何数字、int?>>int

使用checked运算符可以检查类型强制转换是否安全,不安全会迫使运行库抛出一个溢出异常 int i = checked((int)val);

所有的显式转换都可能不安全。如果从可空类型强制转换为非可空类型,且变量的值是null,就会抛出InvalidOperationException异常。

显式类型转换的限制:值类型只能在数字、char类型和enum类型之间转换,不能直接把布尔类型强制转换为其他类型,也不能把其他类型转换为布尔类型。

当需要分析字符串,以检索一个数字或布尔值,可以使用所有预定义值类型都支持的Parse()方法。

拆箱与装箱:值类型和引用类型之间的相互转换

装箱可以隐式地进行也可以显式地转换,拆箱是显式进行的。

当拆箱后得到的值变量无足够空间存储时,会导致一个InvalidCastException的异常。

9.比较对象的相等性

比较引用类型的相等性:

System.Object定义了3个方法来比较对象的相对性,ReferenceEquals()和两个版本的Equals()。再加上比较运算符==。

  • 静态的ReferenceEquals()方法:测试两个引用是否引用类的同一个实例,是否包含内存中的相同地址,是则返回true,但它认为null等于null。
  • 虚拟的Equals()方法:可以比较引用,可以在类中重写从而实现按值比较。如,如果希望类的实例用作字典的键就需要重写此方法。
  • 静态的Equals()方法:与虚拟的版本作用相同,区别是带有两个参数,并对它们进行相等性比较。这个方法可以处理两个对象中有一个是null的情况。当有一个为null,可以抛出异常。
  • 比较运算符(==):看做严格的值比较和严格的引用比较之间的中间选项。可以重写比较运算符(运算符重载),以执行值的比较,如System.String(微软重写了).

比较值类型的相等性:

 在比较值类型的相等性时,采用与引用类型相同的规则:

  • ReferenceEquals()用于比较引用,在比较值类型时,总是返回false,需要将值类型装箱才能比较引用。如bool b = ReferenceEquals(v,v);但是v会被单独装箱,所以结果仍为false,所以用它来比较值类型不好。
  • Equals()用于比较值:微软在System.ValueType类中重载了实例方法Equals(),以便对值类型进行合适的相等性测试。如sA.Equals(sB);sA和sB是某个结构的实例,根据sA和sB是否在其所有的字段中包含相同的值返回true或false。也可重写,以提高性能。如果值类型包含作为字段的引用类型就需要重写,因为默认只比较地址。
  • 比较运算符:值类型需要装箱,才能转换为引用,进而才能对它们执行方法。

10.运算符重载

重载不仅仅限于算术运算符,还需要考虑比较运算符==、<、>、!=、>=、<=

==运算符:默认比较内存地址、对于string,则重写为比较值、对于值类型默认不工作并产生编译错误,除非显式重载

运算符实现过程:查找最佳匹配的运算符重载版本>>查找可以将某个运算符重载版本参数隐式转换成当前参数的版本>>找不到则产生编译错误

运算符用于结构或类时,工作方式是一样的,如

//x表示到原点x方向距离,y表示到原点y方向距离,z表示高度
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 "(" + "," + y + "," + z +")";
    }

    //运算符重载方法    
    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关键字:自定义的运算符重载。一般将运算符左边的参数命名为lhs,右边的的命名为rhs。

C#要求所有的运算符重载都声明为public和static,因此运算符重载的代码体不能访问非静态类成员,也不能访问this标识符。

与C++不同,C#不允许重载“=”运算符,但如果重载“+”运算符,编译器就会自动使用“+”运算符的重载来执行“+=”运算符的操作。-=、&=、*=、/=等所有赋值运算符也遵循此规则。

比较运算符的重载:

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

  • ==和!=
  • >和<
  • >=和<=

C#语言要求成对重载比较运算符。即,如果重载了"==",也就必须重载"!=",否则会产生编译错误。

比较运算符必须返回布尔类型的值,否则没有意义。这是与算术运算符的根本区别。

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;
}

 

还需要重载运算符"!=",

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

 

浅度比较式比较对象是否执行内存中的同一个位置,而深度比较是比较对象的值和属性是否相等。

运算符

可重载性

+-!~++--true 和 false

可以重载这些一元运算符。

+-*/%&| ^<<>>

可以重载这些二进制运算符。

==!=<><=>=

比较运算符可以重载(但请参见本表后面的说明)。

&&||

条件逻辑运算符不能重载,但可使用能够重载的 & 和 | 进行计算。

[]

不能重载数组索引运算符,但可定义索引器。

(T)x

不能重载转换运算符,但可定义新的转换运算符(请参见 explicit 和implicit)。

+=-=*=/=%=&=|=^=<<=>>=

赋值运算符不能重载,但 += 可使用 + 计算,等等。

=, .,?:,??, - AMP_GT, =AMP_GT, f f(x),, checkeduncheckeddefaultdelegateisnewsizeof, typeof

不能重载这些运算符。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

说明 说明

比较运算符(如果重载)必须成对重载;也就是说,如果重载 ==,也必须重载 !=。 反之亦然,< 和 > 以及 <= 和 >= 同样如此。

 

 

 

11.用户定义的类型强制转换

C#允许支持在自定义的数据类型之间进行类型强制转换。方法是把类型强制转换运算符定义为相关类的一个成员运算符,类型强制转换运算符必须标记为隐式或显式。

  • 如果知道无论在源变量中存储什么值,类型强制转换总是安全的,就可以把它定义为隐式强制转换。//implicit
  • 如果某些数值可能会出错,如丢失数据或抛出异常,就可以把它定义为显式强制转换。//explicit

定义类型强制转换的语法如下,必须同时声明为public和static,下面代码表示可以将Currenty对象隐式转换为float

public static implicit operator float(Currency value)
{
    //processing
}

 示例

struct Currency
{
    public uint Dollars;
    public ushort Cents;

    public Currency(uint dollars,ushort cents)
    {
        this.Dollars = dollars;
        this.Cents = cents;
    }

    public override string ToString()
    {
        return string.Format("${0}.{1,-2:00}",Dollars,Cents);
    }
    
    //隐式转换
    public static implicit operator float(Currency value)
    {
        return value.Dollars + (value.Cents/100.0f);
    }

    //显式转换,但这样写会产生小数舍入错误,转换成ushort将截断(详见188页案例说明)
    //public static explicit operator Currentcy(float value)
    //{
        //uint dollars = (uint)value;
        //ushort cents = (ushort)((value - dollars) * 100);
        //return new Currency(dollars,cents);
    //}

    //显式转换,将四舍五入转换为uInt16,使用checked抛出溢出异常
    public static explicit operator Currency(float value)
    {
       checked
       {
          uint dollars = (uint)value;
          ushort cents = Convert.ToUInt16((value - dollars) * 100);
          return new Currency(dollars, cents);
       }
    }
}

 

使用checked检测溢出异常的代码不应该写在调用强制类型转换时,应给像上例一样放在强制转换方法体中,因为这是在强制转换运算符的代码中发生的。

类之间的类型强制转换:

定义不同的结构或类的实例之间的类型强制转换有两个限制:

  • 如果某个类派生自另一个类,就不能定义这两个类之间的类型强制转换(这些类型的类型转换已经存在)
  • 类型强制转换必须在源数据类型或目标数据类型的内部定义

如以下层次结构,从上到下依次继承,则唯一合法的自定义类型强制转换就是类C和D之间的转换,因为它们没有互相派生。

System.Object

    |

    A

    |

    B

  /    \

C        D

如:定义可以放在C的类定义内部,或者在D的类定义内部,但不能放在其他地方定义。一旦在一个类中定义,就不能在另一个类中定义。

public static explicit operator D(C value){...}

 

基类和派生类之间的类型强制转换:

MyDerived derivedObject = new MyDerived();
MyBase baseCopy = derivedObject;

 

从派生类隐式地强制转换为基类是可以的,因为对基类的任何引用都可以引用基类或派生类的对象。

将基类显式强制转换为派生类是不可以的,比较合适的方法是将基类作为参数在初始化派生类时传入,让构造函数完成初始化。

装箱和拆箱数据类型强制转换:

值类型与引用类型一致,都是支持从派生类到基类的隐式转换。本质上进行了装箱。

在使用装箱和拆箱时,这两个过程都把数据复制到新装箱和拆箱的对象上。

12.多重类型强制转换

如果在进行要求的数据类型转换时,C#编译器没有可用的直接强制转换方式,编译器会寻找一种转换方式,把几种强制转换合并起来。如,

Currency balance = new Currency(10,50);
long amount = (long)balance;
double amoutD = balance;

 

因为编译器知道存在Currency到float的隐式转换,而且它知道怎么从float显式地转换为long。

转换当存在多条路径时,C#有一些严格的规则(详见MSDN),告诉编译器该如何确定哪条是最佳路径,但最好自己设计类型强制转换,让所有转换都得到相同的结果(但没精度损失)。

13.当将类型强制转换和方法重载合并起来时,会出现不希望的错误源

如果方法调用带有多个重载方法,并要给该方法传送参数,而该参数的数据类型不匹配任何重载方法,就可以迫使编译器确定使用哪些强制转换方式进行数据转换,从而决定使用哪个重载方法(并进行相应的数据转换)。当然,编译器总是按逻辑和严格的规则来工作,但结果可能不是我们所期望的。如果存在疑问,最好指定显式地使用哪种强制转换。如,

Currency balance = new Currency(50,35);
Console.WriteLine(balance);//50
Console.WriteLine(balance.ToString());//50.35

//因为Console.WriteLine找不到参数为Currency结构的重载版本,最后使用了参数是unit,显式unit的版本。

 

posted @ 2014-02-19 23:39  阿樂  阅读(548)  评论(0编辑  收藏  举报