目录
一、表达式
运算符是一个符号,它表示返回单个结果的操作。
操作数是指作为运算符输入的数据元素。
运算符会:
- 将操作数作为输入;
- 执行某个操作;
- 基于该操作返回一个值;
表达式是运算符和操作数的字符串。C#运算符有一个、两个或三个操作数,可以作为操作数的结构有:
- 字面量
- 常量;
- 变量;
- 方法调用;
- 元素访问器,如数组访问器和索引器;
- 其他表达式;
可以使用运算符组合表达式以创建更复杂的表达式,如下面的表达式所示,有3个运算符和4个操作数:
表达式求值是将每个运算符以适当的顺序应用到它的操作数以产生一个值的过程:
- 值被返回到表达式求值的位置,在那里,它可能是一个封闭的表达式的操作数;
- 除了返回值以外,一些表达式还有副作用,比如在内存中设置一个值;
二、字面量
字面量是源代码中键入的数字或字符串,表示一个指定类型的明确的、固定的值。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("{0}", 1024); //整数字面量
Console.WriteLine("{0}", 3.1416); //双精度型字面量
Console.WriteLine("{0}", 3.1416F); //浮点型字面量
Console.WriteLine("{0}", true); //布尔型字面量
Console.WriteLine("{0}", 'x'); //字符型字面量
Console.WriteLine("{0}", "Hi there"); //字符串字面量
}
}
执行结果:
注意double字面量和float字面量的区别。
因为字面量是写进源代码的,所以它们的值必须在编译时可知。几个预定义类型有自己的字面量形式。
- bool有两个字面量:true和false,注意,像所有C#关键字一样,它们是小写的;
- 对于引用类型变量,字面量null表示变量没有指向内存中的数据;
1、整数字面量
整数字面量是最常用的字面量,它们被书写为十进制数字序列,并且:
- 没有小数点;
- 带有可选的后缀,指明整数的类型;
例如,下面几行展示了4个字面量,都是整数236,依据它们的后缀,每个常数都被编译器解释为不同的整数类型:
- 236 整型
- 236L 长整型
- 236U 无符号整型
- 236UL 无符号长整型
整数类型字面量还可以写成十六进制(hex)形式,数字必须是十六进制数(从0到F),而且字符串必须以0x或0X开始(数字0,字母x)。
整数类型字面量的第三种格式是二进制记法,所有的数字必须是0或1,并且必须以0b或者0B开始(数字0,字母b)。
说明:
只有整数类型字面量可以用十六进制或二进制格式表示,十六进制和二进制记法用前缀指定,而实际的数据类型用后缀指定。
2、实数字面量
C#有三种实数数据类型:float、double和decimal,它们分别对应32位、64位和128位精度。这三种都是浮点数据类型,这意味着它们在内部由两个部分组成,其中一部分是实际的数字,另一部分则是表示小数点位置的指数。在实际使用中,double是到目前为止最常用的实数数据类型。
实数字面量的组成如下:
- 十进制数字;
- 一个可选的小数点;
- 一个可选的指数部分;
- 一个可选的后缀;
float f1 = 236F;
double d1 = 236.714;
double d2 = .35192;
double d3 = 6.338e-26;
说明:
无后缀的实数字面量是double类型,不是float类型!
3、字符字面量
字符字面量由两个单引号内的字符组成,字符字面量用于表示单个字符(a)、非打印字符(\n)(换行符)或者执行特殊任务的字符,例如转义(\\)。尽可能需要多个字符来表示一个字符字面量,但每个字符字面量只表示一个字符。要引用多个字符,把必须使用字符串字面量。
字符字面量可以是下面任意一种:单个字符、一个简单转义序列、一个十六进制转义序列或一个Unicode转义序列。
关于字符字面量需要知道以下几点:
- 字符字面量的类型是char;
- 简单转义序列是一个反斜杠后面跟着单个字符;
- 十六进制转义序列是一个反斜杠后面跟着一个大写或小写的x,再跟着4个十六进制数字;
- Unicode转义序列是一个反斜杠后面跟着一个大写或小写的u,再跟着4个十六进制数字;
char c1 = 'd'; //单个字符
char c2 = '\n'; //简单转义序列
char c3 = '\x0061'; //十六进制转义序列
char c3 = '\u005a'; //Unicode转义序列
4、字符串字面量
字符串字面量使用双引号标记,不同于字符字面量使用单引号,有两种字符串字面量类型:
- 常规字符串字面量;
- 逐字字符串字面量;
常规字符串字面量由双引号内的字符序列组成,常规字符串字面量可以包含:
- 字符;
- 简单转义序列;
- 十六进制和Unicode转义序列;
逐字字符串字面量的书写如同常规字符串字面量,但它以一个@字符为前缀,逐字字符串字面量有以下重要特征:
- 逐字字符串字面量与常规字符串字面量的区别在于字符串中的转义序列不会被求值,在双引号中间的所有内容,包括通常被认为是转义序列的内容,都被严格按字符串中列出的那样打印;
- 逐字字符串字面量的唯一例外是相邻的双引号组,它们被解释为单个双引号字符;
string rst1="Hi there!";
string vst1=@"Hi there!";
string rst2="It started,\"Four score and seven...\"";
string vst2=@"It started,""Four score and seven...""";
string rst3="Value 1 \t 5,val2 \t 10";
string vst3=@"Value 1 \t 5,val2 \t 10";
string rst4="C:\\Program Files\\Microsoft\\";
string vst4=@"C:\Program Files\Microsoft\";
string rst5=" Print \x000A Multiple \u000A Lines";
string vst5=@" Print
Multiple
Lines";
说明:
编译器让相同的字符串字面量共享堆中同一内存位置以节约内存。
三、求值顺序
表达式可以由许多嵌套的子表达式构成,子表达式的求值顺序可以使表达式的最终值发生变化。
1、优先级
运算符优先级:从高到低
2、结合性
假设编译器正在计算一个表达式,且该表达式中所有运算符都有不同的优先级,那么编译器将计算每个子表达式,从级别最高的开始,按优先等级从高到低一直计算你下去。
但如果两个连续的运算符有相同的优先级别怎么办?当连续的运算符有相同的优先级时,求值顺序由操作结合性决定,也就是说,一至两个相同优先级的运算符,依照运算符的结合性,其中的一个或另一个优先。运算符结合性的一些重要特征如下所示:
- 左结合运算符从左至右求值;
- 右结合运算符从右至左求值;
- 除赋值运算符以外,其他二元运算符是左结合的;
- 赋值运算符和条件运算符是右结合的;
可以使用圆括号显式地设定子表达式地求值顺序。括号内的子表达式:
- 覆盖优先级和结合性规则;
- 求值顺序从嵌套的最内层到最外层;
四、简单算术运算符
五、求余运算符
六、关系比较运算符和相等比较运算符
比较操作和相等性操作
对于大多数引用类型来说,比较他们的相等性时,只比较它们的引用:
- 如果引用相等,也就是说,如果它们指向内存中相同的对象,那么相等性比较为true,否则为false,即使内存中两个分离的对象再所有其他方面都完全相等;
- 这成为浅比较;
如上图:
- 左边,a和b的引用是相同的,所以比较会返回true;
- 右边,引用不同,所以即使两个AClass对象的内容完全相同,比较也会返回false;
string类型对象也是引用类型,但它的比较方式不同,比较字符串的相等性时,将比较它们的长度和内容(区分大小写):
- 如果两个字符串有相同的长度和内容(区分大小写),那么相等性比较返回true,即使它们占用不同内存区域;
- 这称为深比较;
委托也是引用类型,并且也使用深比较,比较委托的相等性时,如果两个委托都是null,或两者的调用列表中有相同数目的成员,并且调用列表相匹配,那么比较返回true。
比较数值表达式时,将比较类型和值。比较enum类型时,将比较操作数的实际值。
七、递增运算符和递减运算符
比较这两种运算符的前置和后置形式:
- 无论运算符使用前置形式还是后置形式,在语句执行之后,最终存放在操作数的变量中的值是相同的;
- 唯一不同的是运算符返回给表达式的值;
八、条件逻辑运算符
九、逻辑运算符
十、移位运算符
十一、赋值运算符
十二、条件运算符(三目运算符)
十三、一元算术运算符
十四、用户定义的类型转换
用户定义的转换也是运算符:
- 可以为自己的类和结构定义隐式转换和显式转换,这允许把用户定义类型的对象转换成某个其他类型,反之亦然;
- C#提供隐式转换和显式转换:
- 对于隐式转换,当决定在特定上下文中使用特定类型时,如有必要,编译器会自动执行转换;
- 对于显式转换,编译器只在使用显式转换运算符时才执行转换;
声明隐式转换的语法如下:
public static implicit operator TargetType(SourceType Identifier)
{
……
return ObjectOfTargetType;
}
其中,public和static修饰符是所有用户定义的转换所必需的,TargetType是目标类型,SourceType Identifier是源数据。
显式转换的语法与之相同,但要用explicit替换implicit。
class LimitedInt
{
public static implicit operator int (LimitedInt li) //将LimitedInt转换为int
{
return li.TheValue;
}
public static implicit operator LimitedInt (int x) //将int转换为LimitedInt
{
LimitedInt li = new LimitedInt();
li.TheValue = x;
return li;
}
private int _theValue = 0;
public int TheValue{……}
}
上述代码展示了声明转换运算符的示例,它把类型为LimitedInt的对象转换为int类型,反之亦然。
class LimitedInt
{
const int MaxValue = 100;
const int MinValue = 0;
public static implicit operator int(LimitedInt li) //类型转换
{
return li.TheValue;
}
public static implicit operator LimitedInt(int x) //类型转换
{
LimitedInt li = new LimitedInt();
li.TheValue = x;
return li;
}
private int mTheValue = 0;
public int TheValue
{
get { return mTheValue; }
set
{
if (value < MinValue)
mTheValue = 0;
else
mTheValue = value > MaxValue ? MaxValue : MinValue;
}
}
}
class _14_CustomTypeConversion
{
static void Main()
{
LimitedInt li = 500; //将500转换为LimitedInt
int value = li; //将LimitedInt转换为int
Console.WriteLine($"li:{li.TheValue},value:{value}");
}
}
执行结果:
上述代码再次声明并使用了刚才定义的两个类型转换,在Main中,一个int字面量转换为LimitedInt对象,在下一行,LimitedInt转换成一个int。
显式转换和强制转换运算符
前面的示例代码展示了int到LimitedInt类型的隐式转换和LimitedInt类型到int的隐式转换。然而,如果你把两个转换运算符声明为explicit,将不得不在实行转换时显示使用转换运算符。
强制转换运算符由想要把表达式转换成的目标类型的名称组成,放在一对圆括号内部。如下:
LimitedInt li = (LimitedInt)500; //强制类型转换运算符
class LimitedInt
{
const int MaxValue = 100;
const int MinValue = 0;
public static explicit operator int(LimitedInt li) //类型转换,强制
{
return li.TheValue;
}
public static explicit operator LimitedInt(int x) //类型转换,强制
{
LimitedInt li = new LimitedInt();
li.TheValue = x;
return li;
}
private int mTheValue = 0;
public int TheValue
{
get { return mTheValue; }
set
{
if (value < MinValue)
mTheValue = 0;
else
mTheValue = value > MaxValue ? MaxValue : MinValue;
}
}
}
class _14_2CustomTypeConversion
{
static void Main()
{
LimitedInt li = (LimitedInt)500;
int value = (int)li;
Console.WriteLine($"li:{li.TheValue},value={value}");
}
}
执行结果:
另外有两个运算符,它接受一种类型的值,并返回另一种不同的、指定类型的值,这就是is运算符和as运算符。
十五、运算符重载
C#运算符被定义为使用预定义类型作为操作数来工作,如果面对一个用户定义类型,运算符完全不知道如何处理它,运算符重载允许你定义C#运算符应该如何操作自定义类型的操作数:
- 运算符重载只能用于类和结构;
- 为类或结构重载一个运算符x,可以声明一个名称为operator x的方法并实现它的行为(如:operator +和operator -):
- 一元运算符的重载方法带一个单独的class或struct类型的参数;
- 二元运算符的重载方法带两个参数,其中至少有一个必须是class或struct类型;
public static LimitedInt operator -(LimitedInt x) //一元
public static LimitedInt operator +(LimitedInt x, double y) //二元
运算符重载的方法声明需要:
- 声明必须同时使用static和public的修饰符;
- 运算符必须是要操作的类或结构的成员;
class LimitedInt Return
{
public static LimitedInt operator +(LimitedInt x, double y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue + (int)y;
return li;
}
public static LimitedInt operator -(LimitedInt x)
{
//在这个奇怪的类中,减值就是赋值为0
LimitedInt li = new LimitedInt();
li.TheValue = 0;
return li;
}
……
}
上述代码展示了类LimitedInt的两个重载的运算符:加运算符和减运算符。
1、运算符重载的示例
class LimitedInt
{
const int MaxValue = 100;
const int MinValue = 0;
private int _theValue = 0;
public int TheValue
{
get { return _theValue; }
set
{
if (value < MinValue)
_theValue = 0;
else
_theValue = value > MaxValue ? MaxValue : value;
}
}
public static LimitedInt operator -(LimitedInt x)
{
//在这个奇怪的类中,取一个值的负数等于0
LimitedInt li = new LimitedInt();
li.TheValue = 0;
return li;
}
public static LimitedInt operator -(LimitedInt x,LimitedInt y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue - y.TheValue;
return li;
}
public static LimitedInt operator +(LimitedInt x,double y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue + (int)y;
return li;
}
}
class _15_1OperatorOverloading
{
static void Main()
{
LimitedInt li1 = new LimitedInt();
LimitedInt li2 = new LimitedInt();
LimitedInt li3 = new LimitedInt();
li1.TheValue = 10;li2.TheValue = 26;
Console.WriteLine($"li1:{li1.TheValue},li2:{li2.TheValue}");
li3 = -li1;
Console.WriteLine($"-{li1.TheValue} = {li3.TheValue}");
li3 = li2 - li1;
Console.WriteLine($"{li2.TheValue} - {li1.TheValue} = {li3.TheValue}");
li3 = li1 - li2;
Console.WriteLine($"{li1.TheValue} - {li2.TheValue} = {li3.TheValue}");
}
}
执行结果如下:
2、运算符重载的限制
不是所有的运算符都能被重载,可以重载的类型也有限制,只有下面这些运算符可以被重载:
列表中明显缺少的是赋值运算符。
运算符重载不能:
- 创建新运算符;
- 改变运算符的语法;
- 重新定义运算符如何处理预定义类型;
- 改变运算符的优先级或结合性;
递增运算符和递减运算符也可以重载。
你可以编写一段代码来对对象进行递增或递减操作:
- 在运行时,当你的代码对对象执行前置操作(前置递增或前置递减)时,会发生以下行为:
- 在对象上执行递增或递减代码;
- 返回对象;
- 在运行时,当你的代码对对象执行后置操作(后置递增或后置递减)时,会发生以下行为:
- 如果对象是值类型,则系统会复制该对象;如果对象是引用类型,则引用会被复制;
- 在对象上执行递增或递减代码;
- 返回保存的操作数;
如果你的 操作数对象值类型对象,那么一点问题都没有,但是当你的用户定义类型是引用类型时,你就需要小心了。
对于引用类型的对象,前置操作没有问题,因为没有进行复制。但是,对于后置操作,因为保存的副本是引用的副本,所以这意味着原始引用和引用副本指向相同的对象。那么,当进行第二步操作的时候,递增或者递减代码就会在对象上执行。这意味着保存的引用所指向的对象不再是它的起始状态了。返回对变化了的对象的引用可能不是预期行为。
public struct MyStructType
{
public int X;
public MyStructType(int x)
{
X = x;
}
public static MyStructType operator ++(MyStructType m)
{
m.X++;
return m;
}
}
public class MyClassType
{
public int X;
public MyClassType(int x)
{
X = x;
}
public static MyClassType operator ++(MyClassType m)
{
m.X++;
return m;
}
}
class _15_2OperatorOverloadingLimit
{
static void ShowStruct(string message,MyStructType tv)
{
Console.WriteLine($"{message} {tv.X}");
}
static void ShowClass(string message, MyClassType tv)
{
Console.WriteLine($"{message} {tv.X}");
}
static void Main()
{
MyStructType mst = new MyStructType(10);
Console.WriteLine("Struct Pre-increment");
ShowStruct("Before ", mst);
ShowStruct("Returned ", ++mst);
ShowStruct("After ", mst);
Console.WriteLine();
mst = new MyStructType(10);
Console.WriteLine("Struct Post-increment");
ShowStruct("Before ", mst);
ShowStruct("Returned ", mst++);
ShowStruct("After ", mst);
Console.WriteLine();
MyClassType mct = new MyClassType(10);
Console.WriteLine("Class Pre-increment");
ShowClass("Before ", mct);
ShowClass("Returned ", ++mct);
ShowClass("After ", mct);
Console.WriteLine();
mct = new MyClassType(10);
Console.WriteLine("Class Post-increment");
ShowClass("Before ", mct);
ShowClass("Returned ", mct++);
ShowClass("After ", mct);
}
}
执行结果:
上述代码及执行结果,当结构体执行后置递增时,结果如我所期望的,但是自定义类型即引用类型执行后置递增时,结果似乎跟我所期望的有点出入,因为副本是引用,而递增这个行为不是作用在引用上,而是作用在对象上。
十六、typeof运算符
typeof运算符返回作为其参数的任何类型的System.Type对象,通过这个对象,可以了解类型的特征。对任何已知类型,只有一个System.Type对象。你不能重载typeof运算符。
typeof运算符是一元运算符。
class SomeClass
{
public int Field1;
public int Field2;
public void Method1() { }
public int Method2() { return 1; }
}
class _16_TypeofOperator
{
static void Main()
{
Type t1 = typeof(SomeClass);
FieldInfo[] fi1 = t1.GetFields();
MethodInfo[] mi1 = t1.GetMethods();
foreach (FieldInfo f in fi1)
Console.WriteLine($"Field: {f.Name}");
foreach (MethodInfo m in mi1)
Console.WriteLine($"Method: {m.Name}");
Console.WriteLine();
SomeClass s = new SomeClass();
Type t2 = s.GetType(); //GetType方法也会调用typeof运算符,该方法对每个类型的每个方法都有效
FieldInfo[] fi2 = t2.GetFields();
MethodInfo[] mi2 = t2.GetMethods();
foreach (FieldInfo f in fi2)
Console.WriteLine($"Field: {f.Name}");
foreach (MethodInfo m in mi2)
Console.WriteLine($"Method: {m.Name}");
}
}
执行结果:
十七、nameof运算符
nameof运算符返回一个表示传入参数的字符串。
struct MyStruct
{
}
class MyClass
{
public int Field1;
public int Property1 { get; set; }
public int Method1(int parameter1)
{
Console.WriteLine(nameof(parameter1)); //方法参数("parameter1")
return Property1;
}
}
class _17_NameofOperator
{
static void Main()
{
string var1 = "Local Variable";
Console.WriteLine(nameof(MyClass)); //局部变量("var1")
Console.WriteLine(nameof(MyClass.Method1)); //公有方法
Console.WriteLine(nameof(MyClass.Property1)); //共有属性
Console.WriteLine(nameof(MyClass.Field1)); //公有字段
Console.WriteLine(nameof(MyStruct)); //结构体
Console.WriteLine(nameof(System.Math)); //打印"Math"
Console.WriteLine(nameof(Console.WriteLine)); //打印"Writeline"
}
}
执行结果:
十八、其他运算符
本章介绍的运算符是内置类型的标准运算符。
还有其他的运算符,例如可空类型有两个特殊的运算符,分别叫作空结合运算符和空条件运算符,以后讨论。