文章目录
运算符
运算符简化
条件运算符
条件运算符(?: 也称为三元运算符,是if.else结构的简化形式。其名称的出处是它带有3个操作数。它首
先判断一个条件,如果条件为真,就返回一个值;如果条件为假,则返回另一个值。其语法如下:
condition ? true value: false value
checked和unchecked运算符
C#提供了checked和uchecked运算符。如果把一个代码块标记为checked,CLR就会执行溢出检查,
如果发生溢出,就抛出OverflowException异常。
byte b==255;
checked
{
++;
}
Console.WriteLine(b);
运行这段代码,就会得到一条错误信息:
System.OverflowException:Arithmetic operation resulted in an overflow
is运算符
is运算符可以检查对象是否与特定的类型兼容。短语“兼容”表示对象或者是该类型,或者派生自该类型。
例如,要检查变量是否与object类型兼容,可以使用下面的代码
int i=10;
if (i is object)
{
Console.Writeline("i is an object");
}
as运算符
as运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行:如果类型不兼容,as运算符就会返回null值。
private static void AsOperatorSample()
{
Console.WriteLine(nameof(AsOperatorSample));
object o1 = "Some String";
object o2 = 5;
string s1 = o1 as string; // s1 = "Some String"
string s2 = o2 as string; // s2 = null
Console.WriteLine($"o1 as string assigns a string to s1: {s1}");
Console.WriteLine($"o2 as string assigns null to s2 because o2 is not a string: {s2}");
Console.WriteLine();
}
as运算符允许在一步中进行安全的类型转换,不需要先使用is运算符测试类型,再执行转换
sizeof运算符
使用sizeof运算符可以确定栈中值类型需要的长度(单位是字节)
private static void SizeofSample()
{
Console.WriteLine(nameof(SizeofSample));
Console.WriteLine(sizeof(int));
unsafe
{
Console.WriteLine(sizeof(Point));
}
Console.WriteLine();
}
其结果是显示数字4,因为it有4个字节长。
如果结构体只包含值类型,也可以使用sizeof运算符和结构
public struct Point
{
public Point(int x, int y)
{
X = x;
Y = y;
}
public int X { get; }
public int Y { get; }
}
注意:
类不能使用sizeof运算符,如果对复杂类型(而非基本类型)使用sizeof运算符,就需要把代码放在unsafe块中,如下所示(代码文件
private static void SizeofSample()
{
Console.WriteLine(nameof(SizeofSample));
Console.WriteLine(sizeof(int));
unsafe
{
Console.WriteLine(sizeof(Point));
}
Console.WriteLine();
}
typeof运算符
typeof运算符返回一个表示特定类型的System.Type对象。例如,typeofstring)返回表示System.String
类型的Type对象。在使用反射技术动态地查找对象的相关信息时,这个运算符很有用。
nameof运算符
nameof是新的C#6运算符。该运算符接受一个符号、属性或方法,并返回其名称
index运算符
类似于访问数组元素,素引运算符不需要把整数放在括号内,并且可以用任何类型定义。
var dict=new Dictionary<string,int>();
dict["first”] =1;
int x=dict["first”];
可空类型和运算符
值类型和引用类型的一个重要区别是,引用类型可以为空。值类型(如int不能为空。把C#类型映射到数据库类型时,这是一个特殊的问题。数据库中的数值可以为空。在早期的C#版本中,一个解决方案是使用引用类型来映射可空的数据库数值。然而,这种方法会影响性能,因为垃圾收集器需要处理引用类型。现在可以使用可空的int来替代正常的int。其开销只是使用一个额外的布尔值来检查或设置空值。可空类型仍然是值类型。
static void Main()
{
int i1 = 1;
int? i2 = 2;
int? i3 = null;
long? l1 = null;
DateTime? d1 = null;
int? a = null;
int? b = a + 4; // b = null
int? c = a * 5; // c = null
IfCompareToNull();
}
static void IfCompareToNull()
{
int? a = null;
int? b = -5;
if (a >= b) // if a or b is null, this condition is false
{
Console.WriteLine("a >= b");
}
else
{
Console.WriteLine("a < b");
}
}
空合并运算符
空合并运算符(??)提供了一种快捷方式,可以在处理可空类型和引用类型时表示null值的可能性。这个运算符放在两个操作数之间,第一个操作数必须是一个可空类型或引用类型;第二个操作数必须与第一个操作数的类型相同,或者可以隐式地转换为第一个操作数的类型。空合并运算符的计算如下:
- 如果第一个操作数不是null,整个表达式就等于第一个操作数的值。
- 如果第一个操作数是null,整个表达式就等于第二个操作数的值。
例如:
int? a= null;
int b;
b=a ?? 10;
a = 3;
b = a ?? 10;
空值条件运算符
C#中减少大量代码行的一个功能是空值条件运算符。生产环境中的大量代码行都会验证空值条件。访问作为方法参数传递的成员变量之前,需要检查它,以确定该变量的值是否为null,否则会抛出一个
NullReferenceException异常。NET设计准则指定,代码不应该抛出这些类型的异常,应该检查空值条件。
public void ShowPerson(Person p)
{
if(p== null)return i;
string firstName=p.FirstName;
}
运算符的优先级
运算符优先级从下到上优先级递增,最上面的优先级最高
类型安全
C#也支持不同引用类型之间的转换,在与其他类型相互转换时还允许定义所创建的数据类型的行为方式
类型转换
我们常常需要把数据从一种类型转换为另一种类型。考虑下面的代码:
byte value1 = 10;
byte value2 = 23;
byte total;
total = valuel+value2;
Console.WriteLine(total);
在试图编译这些代码行时,会得到一条错误消息Cannot implicitly convert type ‘int’ tolbyte
问题是,把两个byte型数据加在一起时,应返回int型结果,而不是另一个byte数据。这是因为byte包含
的数据只能为8位,所以把两个byte型数据加在一起,很容易得到不能存储在单个byte型数据中的值。如果要把结果存储在一个byte变量中,就必须把它转换回byte类型。C#支持两种转换方式:隐式转换和显式转换。
隐式转换
只要能保证值不会发生任何变化,类型转换就可以自动(隐式)进行。这就是前面代码失败的原因:试图从int转换为byte,而可能丢失了3个字节的数据。编译器不允许这么做,除非我们明确告诉它这就是我们希望的结果!如果在long类型变量而非byte类型变量中存储结果,就不会有问题了:
byte valuel = 10;
byte value2= 23;
long total;
total = valuel+value2;
Console.WriteLine(total);
程序可以顺利编译,而没有任何错误,这是因为long类型变量包含的数据字节比byte类型多,所以没有丢失数据的危险。在这些情况下,编译器会很顺利地转换,我们也不需要显式地提出要求。
注意,只能从较小的整数类型隐式地转换为较大的整数类型,而不能从较大的整数类型隐式地转换为较小的整数类型。也可以在整数和浮点数之间转换;然而,其规则略有不同。尽管可以在相同大小的类型之间转换,如intuint转换为float,long/ulong转换为double,也可以从long/ulong转换回float。这样做可能会丢失4个字节的数据,但这仅表示得到的float值比使用double得到的值精度低:编译器认为这是一种可以接受的错误,因为值的数量级不会受到影响。还可以将无符号的变量分配给有符号的变量,只要无符号变量值的大小在有符号变量的范围之内即可。
在隐式地转换值类型时,对于可空类型需要考虑其他因素:
- 可空类型隐式地转换为其他可空类型,应遵循表中非可空类型的转换规则。即int?隐式地转换为
long?、float?、double?和 decimal?。 - 非可空类型隐式地转换为可空类型也遵循表6-4中的转换规则,即int隐式地转换为long?、float?、double? 和decimal?
- 可空类型不能隐式地转换为非可空类型,此时必须进行显式转换,如下一节所述。这是因为可空类型 的值可以是null,但非可空类型不能表示这个值
显式转换
有许多场合不能隐式地转换类型,否则编译器会报告错误。下面是不能进行隐式转换的一些场合:
- int转换为short会丢失数据
- int转换为uint-会丢失数据
- uint转换为int会丢失数据
- float转换为int-会丢失小数点后面的所有数据
- 任何数字类型转换为char—会丢失数据
- decimal转换为任何数字类型——因为decimal类型的内部结构不同于整数和浮点数
- int?转换为int-一可空类型的值可以是null
但是,可以使用类型强制转换(cast显式地执行这些转换。在把一种类型强制转换为另一种类型时,有意地迫使编译器进行转换。类型强制转换的一般语法如下:
long val = 3000;
int i = (int)val;
这表示,把强制转换的目标类型名放在要转换值之前的圆括号中。对于熟悉C的程序员,这是类型强制转换的典型语法。这种类型强制转换是一种比较危险的操作,即使在从long转换为int这样简单的类型强制转换过程中,果原来long的值比int的最大值还大,就会出现问题。
比较对象的相等性
对象相等的机制有所不同,这取决于比较的是引用类型(类的实例)还是值类型(基本数据类型、结构或枚举的实例)。
比较引用类型的相等性
ReferenceEquals()方法
ReferenceEquals0是一个静态方法,其测试两个引用是否指向类的同一个实例,特别是两个引用是否包含内存中的相同地址。作为静态方法,它不能重写,所以System.Object的实现代码保持不变。如果提供的两个引用指向同一个对象实例,则ReferenceEqualsO总是返回true;否则就返回flse。但是,它认为null等于null.
private static void ReferenceEqualsSample()
{
SomeClass x = new SomeClass(), y = new SomeClass(), z = x;
bool b1 = object.ReferenceEquals(null, null); // returns true
bool b2 = object.ReferenceEquals(null, x); // returns false
bool b3 = object.ReferenceEquals(x, y); // returns false because x and y
// reference different objects
bool b4 = object.ReferenceEquals(x, z); // returns true references the same object
}
Equals()虚方法
Equals()虚版本的System.Object实现代码也可以比较引用。但因为这是虚方法,所以可以在自己的类中重写它,从而按值来比较对象。特别是如果希望类的实例用作字典中的键,就需要重写这个方法,以比较相关值。否则,根据重写Object.GetHashCode()的方式,包含对象的字典类要么不工作,要么工作的效率非常低。在重写Equals0方法时要注意,重写的代码不应抛出异常。同理,这是因为如果抛出异常,字典类就会出问题,一些在
内部调用这个方法的.NET基类也可能出问题。
静态的Equals()方法
Equals()的静态版本与其虚实例版本的作用相同,其区别是静态版本带有两个参数,并对它们进行相等性比较。这个方法可以处理两个对象中有一个是null的情况:因此,如果一个对象可能是mull,这个方法就可以抛出异常,提供额外的保护。静态重载版本首先要检查传递给它的引用是否为null。如果它们都是null,就返回true(因为null与null相等)。如果只有一个引用是null,它就返回lse。如果两个引用实际上引用了某个对象,它就调用EqualsO的虚实例版本。这表示在重写EqualsO的实例版本时,其效果相当于也重写了静态版本。
比较运算符(==)
最好将比较运算符看作严格的值比较和严格的引用比较之间的中间选项。在大多数情况下,下面的代码表示正在比较引用:
bool b = (x == y); //x.y obiect
但是,如果把一些类看作值,其含义就会比较直观,这是可以接受的方法。在这些情况下,最好重写比较运算符,以执行值的比较。后面将讨论运算符的重载,但一个明显例子是System.Sting类,Microsof重写了这个运算符,以比较字符串的内容,而不是比较它们的引用。
比较值类型的相等性
在比较值类型的相等性时,采用与引用类型相同的规则:ReferenceEquals()用于比较引用,Equals()用于比较值,比较运算符可以看作一个中间项。但最大的区别是值类型需要装箱,才能把它们转换为引用,进而才能对它们执行方法。另外,Mierosoft已经在System.ValueType类中重载了实例方法EqualsO,以便对值类型进行合适的相等性测试。如果调用sA.Equals(sB),其中sA和sB是某个结构的实例,则根据SA和SB是否在其所有的字段中包含相同的值而返回tue或false。另一方面,在默认情况下,不能对自己的结构重载-运算符。在表达式中使用(SASB)会导致一个编译错误,除非在代码中为当前的结构提供了的重载版本。另外,ReferenceEquals()在应用于值类型时总是返回false,因为为了调用这个方法,值类型需要装箱到对象中。即使编写下面的代码
bool b= ReferenceEquals(v,v)://v is a variable of some value type
也会返回false,因为在转换每个参数时,v都会被单独装箱,这意味着会得到不同的引用。出于上述原因,调用ReferenceEqualsO来比较值类型实际上没有什么意义,所以不能调用它尽管SystemValueType提供的Equals()默认重写版本肯定足以应付绝大多数自定义的结构,但仍可以针对自己的结构再次重写它,以提高性能。另外,如果值类型包含作为字段的引用类型,就需要重写Equals(),以便为这些字段提供合适的语义,因为EqualsO的默认重写版本仅比较它们的地址。
运算符重载
运算符重载的关键是在对象上不能总是只调用方法或属性,有时还需要做一些其他工作,例如对数值进行相加、相乘或逻辑操作(如比较对象)等。
算术运算符重载
struct Vector
{
public Vector(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
public Vector(Vector v)
{
X = v.X;
Y = v.Y;
Z = v.Z;
}
public double X { get; }
public double Y { get; }
public double Z { get; }
public override string ToString() => $"( {X}, {Y}, {Z} )";
public static Vector operator +(Vector left, Vector right) =>
new Vector(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
}
```csharp
public class Program
{
public 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}");
Console.WriteLine($"vect2 = {vect2}");
Console.WriteLine($"vect3 = {vect3}");
}
}
运算符重载的声明方式与静态方法基本相同,但operator关键字告诉编译器,它实际上是一个自定义的运算符重载,后面是相关运算符的实际符号,在本例中就是+。返回类型是在使用这个运算符时获得的类型。在本例中,把两个矢量加起来会得到另一个天量,所以返回类型也是Vector。对于这个特定的+运算符重载,返回类型与包含的类一样,但并不一定是这种情况,在本示例中稍后将看到。两个参数就是要操作的对象。对于二元运算符(带两个参数),如+和-运算符,第一个参数是运算符左边的值,第二个参数是运算符右边的值这个实现代码返回一个新的关量,该失量用left和right变量的X、Y和Z属性初始化。C#要求所有的运算符重载都声明为public和static,这表示它们与其类或结构相关联,而不是与某个特定实列相关联,所以运算符重载的代码体不能访问非静态类成员,也不能访问this标识符:这是可行的,因为参数提供了运算符执行其任务所需要知道的所有输入数据。
### 比较运算符重载
C#语言要求成对重载比较运算符。即,如果重载了一,也就必须重载!,否则会产生编译器错误。另外,比较运算符必须返回布尔类型的值。这是它们与算术运算符的根本区别。例如,两个数相加或相减的结果理论上取决于这些数值的类型。相比之下,如果比较运算得到的不是布尔类型的值,就没有任何意义。如,如果只比较两个对象引用,就是比较存储对象的内存地址。比较运算符很少进行这样的比较,所以必须编写代码重载运算符,比较对象的值,并返回相应的布尔结果。下面对Vector结构重载=和运算符。
```csharp
public struct Vector : IEquatable<Vector>
{
private readonly double X, Y, Z;
public Vector(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
public Vector(Vector v)
{
X = v.X;
Y = v.Y;
Z = v.Z;
}
public override string ToString() => $"( {X}, {Y}, {Z} )";
public static bool operator ==(Vector left, Vector right)
{
if (ReferenceEquals(left, right)) return true;
return left.X == right.X && left.Y == right.Y && left.Z == right.Z;
}
public static bool operator !=(Vector lhs, Vector rhs) =>
!(lhs == rhs);
public override bool Equals(object obj)
{
if (obj == null) return false;
return this == (Vector)obj;
}
public override int GetHashCode() =>
X.GetHashCode() + (Y.GetHashCode() << 4) + (Z.GetHashCode() << 8);
public bool Equals(Vector other) => this == other;
public static Vector operator +(Vector left, Vector right) =>
new Vector(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
public static Vector operator *(double left, Vector right) =>
new Vector(left * right.X, left * right.Y, left * right.Z);
public static Vector operator *(Vector lhs, double rhs) =>
rhs * lhs;
public static double operator *(Vector left, Vector right) =>
left.X * right.X + left.Y * right.Y + left.Z * right.Z;
}
可以重载的运算符
自定义的索引运算符
自定义索引器不能使用运算符重载语法来实现,但是它们可以用与属性非常相似的语法来实现。
public class Person
{
public DateTime Birthday { get; }
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName, DateTime birthDay)
{
FirstName = firstName;
LastName = lastName;
Birthday = birthDay;
}
public override string ToString() => $"{FirstName} {LastName}";
}
public class PersonCollection
{
private Person[] _people;
public PersonCollection(params Person[] people) =>
_people = people.ToArray();
public Person this[int index]
{
get => _people[index];
set => _people[index] = value;
}
public IEnumerable<Person> this[DateTime birthDay] => _people.Where(p => p.Birthday == birthDay);
}
class Program
{
static void Main()
{
var p1 = new Person("Ayrton", "Senna", new DateTime(1960, 3, 21));
var p2 = new Person("Ronnie", "Peterson", new DateTime(1944, 2, 14));
var p3 = new Person("Jochen", "Rindt", new DateTime(1942, 4, 18));
var p4 = new Person("Francois", "Cevert", new DateTime(1944, 2, 25));
var coll = new PersonCollection(p1, p2, p3, p4);
Console.WriteLine(coll[2]);
foreach (var r in coll[new DateTime(1960, 3, 21)])
{
Console.WriteLine(r);
}
Console.ReadLine();
}
}
自定义数据类型转换
C#允许定义自己的数据类型(结构和类),这意味着需要某些工具支持在自定义的数据类型之间进行类型强制转换。方法是把类型强制转换运算符定义为相关类的一个成员运算符。类型强制转换运算符必须标记为隐式或显式,以说明希望如何使用它。我们应遵循与预定义的类型强制转换相同的指导原则:如果知道无论在源变量中存储什么值,类型强制转换总是安全的,就可以把它定义为隐式强制转换。然而,如果某些数值可能会出错,如丢失数据或抛出异常,就应把数据类型转换定义为显式强制转换。
public struct Currency
{
public uint Dollars { get; }
public ushort Cents { get; }
public Currency(uint dollars, ushort cents)
{
Dollars = dollars;
Cents = cents;
}
public override string ToString() => $"${Dollars}.{Cents,-2:00}";
public static implicit operator float (Currency value) =>
value.Dollars + (value.Cents / 100.0f);
public static explicit operator Currency(float value)
{
version 1
//uint dollars = (uint)value;
//ushort cents = (ushort)((value - dollars) * 100);
//return new Currency(dollars, cents);
// version 2
checked
{
uint dollars = (uint)value;
ushort cents = Convert.ToUInt16((value - dollars) * 100);
return new Currency(dollars, cents);
}
}
public static implicit operator Currency(uint value) => new Currency(value, 0);
public static implicit operator uint (Currency value) => value.Dollars;
}