运算符
C#中支持的运算符
类别 | 运算符 |
---|---|
算数运算符 | + - * / % |
逻辑运算符 | & | ^ && || ! |
字符串连接运算符 | + |
递增和递减运算符 | ++ -- |
移位运算符 | >> << |
比较运算符 | == != <> <=> = |
赋值运算符 | = += -+ *= /= %= &= |= ^= <<= >>= |
成员访问运算符(用于对象和结构) | . |
索引运算符(用于数组和索引器) | [] |
类型转换运算符 | () |
条件运算符(三元运算符) | ?: |
委托连接和删除运算符 | + - |
对象创建运算符 | new |
类型信息运算符 | sizeof is typeof as |
溢出溢出控制运算符 | checked unchecked |
间接寻址运算符 | [] |
名称空间别名限定符 | :: |
空合并运算符 | ?? |
空值传播运算符 | ?. ?[] |
标识符的名称运算符 | nameof() |
其中 sizeof * -> 和 & 只能用于不安全的代码。
C#中很少使用指针,因此也很少用到间接寻址运算符 -> 。使用它们的唯一场合是在不安全的代码块中。
运算符的简化操作
C#中的全部简化赋值运算符
简化运算符 | 等价于 |
---|---|
x++,++x | x = x + 1 |
x--,--x | x = x - 1 |
x += y | x = x + y |
x -= y | x = x - y |
x *= y | x = x * y |
x /= y | x = x / y |
x %= y | x = x% y |
x >>= y | x = x >> y |
x <<= y | x = x<< y |
x &= y | x = x & y |
x |= y | x = x | y |
对于x++,++x;将运算符放在表达式前面称为前置,把运算符放在表达式后面称为后置。
当 x++,++x放在单独一行时它们的作用是相同的,但是当它们用于较长的表达式内部时把运算符放在前面会在计算表达式之前递增x,将运算符放在后面会在计算表达式之后计算。
using static System.Console;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
int a = 3;
int b = 5;
WriteLine(a);//3
int c = a++ * b;
WriteLine(a);//4
int d = ++a * b;
WriteLine(a);//5
WriteLine(c);//15
WriteLine(d);//25
ReadKey();
}
}
}
这里要注意一点,无论是a++还是++a,本质都是a = a + 1,都会对a的值进行改变。
但是这里++在前面参与整个表达式运算,但是在后面会在表达式计算完成后对a的值进行运算。
条件运算符
条件运算符 (?:) 也称为三元运算符。是if else的简化形式,其名称的出处是它带有三个参数,首先判断一个条件,如果为真,就返回一个值,为假返回另一个值。
判断条件 ? 为真的值 : 为假的值
checked 和 unchecked
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
byte b = byte.MaxValue;//获取byte类型的最大值
b++;//加一
WriteLine(b);
ReadKey();
}
}
}
最后得到的结果为0。是因为b递增溢出了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace ConsoleApp12
{
class Program
{
static void Main(string[] args)
{
byte b = byte.MaxValue;
checked//溢出检查
{
b++;
}
WriteLine(b);
ReadKey();
}
}
}
运行这段代码将会得到一个异常,提示你发生了溢出。
使用 checked 对代码块进行溢出检查。
而如果不想做溢出检查可以在代码块使用 unchecked 。
unchecked 其实是默认的,可以不写。
is运算符
is 运算符可以检查对象是否与特定类型 兼容。
兼容 表示对象或者是该类型,或者派生自该类型。
using System;
using static System.Console;
namespace ConsoleApp12
{
class Program
{
static void Main(string[] args)
{
int i = 10;
if (i is object)
{
WriteLine("OK");
}
ReadKey();
}
}
}
这里的结果是OK,因为nt类型object类型。
as运算符
as 运算符用于执行引用类型的显示类型转换。
如果类型兼容转换成功进行。
如果类型不兼容返回 null。
using System;
using static System.Console;
namespace ConsoleApp13
{
class Program
{
static void Main(string[] args)
{
object o1 = "asasdsadaswdawsdwd";//字符串类型
object o2 = 777;//整形
string s1 = o1 as string;
string s2 = o2 as string;
WriteLine(s1.GetType());
WriteLine(s1);
WriteLine(s2.GetType());
WriteLine(s2);
ReadKey();
}
}
}
s1 类型兼容会变成string,
s2 类型不兼容,o2为int类型,不能转换为string类型,使用as转换会变为 null.
sizeof运算符
使用 sizeof 运算符可以确定 栈中值类型 需要的长度。单位是字节
WriteLine(sizeof(int));
如果对非基本类型使用 sizeof 运算符,就需要把代码放在 unsafe 中,表明这样做是不安全的。
unsafe
{
WriteLine(sizeof(非基本类型));
}
typeof运算符
返回一个表示特定类型的 System.Type 对象。
using System;
using static System.Console;
namespace ConsoleApp
{
class Program
{
public class asdw
{
public int a { get; set; }
}
static void Main(string[] args)
{
WriteLine(typeof(asdw));//ConsoleApp.Program+asdw
ReadKey();
}
}
}
在使用反射技术动态的查找对象的相关信息时这个运算符很有用。
nameof运算符
该运算符接受一个符号,属性或方法,并返回其名称。
using System;
using static System.Console;
namespace ConsoleApp
{
class Program
{
class Student
{
public int Roll { get; set; }
public string Name { get; set; }
}
static void Main(string[] args)
{
WriteLine(nameof(Student));//Student
WriteLine(nameof(Student.Roll));//Roll
WriteLine(nameof(Student.Name));//Name
ReadKey();
}
}
}
这里不是很理解,这里已经可以把名字作为参数传入那么返回名字有什么意义。
index运算符
索引运算符,不需要将整数放在括号内,可以使用任意类型定义。
using System;
using static System.Console;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
int[] myArray = { 1, 3, 5, 7, 9 };
WriteLine(myArray[1]);
ReadKey();
}
}
}
可空类型运算符
值类型和引用类型的一个重要区别就是,引用类型可以为空,值类型不能为空。
但是将C#类型映射到数据库时,数据库中的数值可以为空。
因此需要使用 ? 将值类型表示为可空的。
int? x = null;
如果在程序中使用可空类型,就需要考虑 null 值与各种运算符一起使用的影响。
通常可空类型与一元或二元运算符一起使用,只要其中一个操作数为 null ,其结果就是 null
using System;
using static System.Console;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
int? a = null;
int? b = 10;
int? c = a + b;
int? d = a++;
WriteLine(a);
WriteLine(b);//b
WriteLine(c);
WriteLine(d);
ReadKey();
}
}
}
这里只有b返回值,其余的全为 null
在比较可空类型时,只要有个操作数是 null ,比较的结果就是 false 。
即不能因为一个条件是 false ,就认为该条件的对立面是 true。
using System;
using static System.Console;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
int? a = null;
int? b = -5;//负数5
if (a >= b)
{
WriteLine("a >= b");
}
else
{
WriteLine("a < b");//输出这条语句
}
ReadKey();
}
}
}
因为有 null ,所有结果总是为 false。
即使将b改为正数5也是如此。
using System;
using static System.Console;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
int? a = null;
int? b = 5;//正数5
if (a >= b)
{
WriteLine("a >= b");
}
else
{
WriteLine("a < b");//输出这条语句
}
ReadKey();
}
}
}
空合并运算符
空合并运算符 ?? 提供了一种快捷方式,可以在处理可空类型和引用类型时表示 null 值的可能性。
这个运算符放在两个操作数之间,第一个操作数必须是一个可空类型或引用类型,第二个操作数必须与第一个操作数的类型相同,或者可以隐式的转换为第一个操作数的类型。如果第二个操作数不能隐式的转换为第一个操作数的类型,就生成一个编译错误。
如果第一个操作数为 null ,这个表达式就等于第二个操作数的值。
如果第一个操作数不为 null ,这个表达式的值就等于第一个操作数的值。
using System;
using static System.Console;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
int? a = null;
int b;
b = a ?? 10;
WriteLine(b);//10
a = 777;
b = a ?? 10;
WriteLine(b);//777
ReadKey();
}
}
}
第一次空合并a为空,所以b的值取后面,第二次空合并a为777,b取第一个操作数。
空值传播运算符
访问作为方法参数传递成员变量之前需要检测是否为空,否则会抛出异常。
public void ShowPerson(Person p)
{
if (p == null)
{
return;
}
else
{
string name = p.Name;
}
}
还可以使用空值传播运算符 ?.
using System;
using static System.Console;
namespace ConsoleApp15
{
public class Person
{
public string Name { get; set; }
}
class Program
{
public static void ShowPerson(Person p)
{
string name = p?.Name;
WriteLine(name);
}
static void Main(string[] args)
{
Program.ShowPerson(null);
ReadKey();
}
}
}
当x为空时,就只返回 null,而不继续执行表达式的右侧。
注意string是引用类型,尽管对其引用修改时总会创建新的对象,但它是引用类型,引用类型可以为空。
String类型直接继承自Object,这使得它成为一个引用类型。
值类型只将值存放在内存中,这些值类型都存储在堆栈中。
而引用类型的内存单元中只存放内存堆中对象的地址,而对象本身放在内存堆中。如果引用的值类型的值是null,则表示未引用任何对象。
如果想要对一个可能为空的值赋值可以使用 ??
public static void ShowPerson(Person p)
{
string name = p?.Name??"为空";
WriteLine(name);
}
使用空值传播运算符访问 int 类型的属性时,不能把结果直接分配给 int 类型,因为结果可以为空,但是int不能为空。但是可以将结果分配给可空的 int后者使用 ?? 空合并运算符
using System;
using static System.Console;
namespace ConsoleApp15
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
public static void ShowPerson(Person p)
{
string name = p?.Name??"为空";
WriteLine(name);
int age = p?.Age??77;
WriteLine(age);
}
static void Main(string[] args)
{
Program.ShowPerson(null);
ReadKey();
}
}
}
还可以结合多个空值传播运算符
using System;
using static System.Console;
namespace ConsoleApp
{
public class Address
{
public string City { get; set; }
}
public class Person
{
public Address Address { get; set; }
}
class Program
{
public static Person GetPerson()
{
return new Person();
}
static void Main(string[] args)
{
Person p = GetPerson();
string city = p?.Address?.City;//??"为空";
WriteLine(city);
ReadKey();
}
}
}
运算符的优先级和关联性
优先从上到下,从高到低。
组 | 运算符 |
---|---|
基本运算符 | () . [] x++ x-- new typeof sizeof checked unchecked |
一元运算符 | + - ! ~ ++x --x 和数据类型强制转换 |
乘除运算符 | * / % |
加减运算符 | + - |
移位运算符 | << >> |
关系运算符 | < > <= >= is as |
比较运算符 | == != |
按位AND运算 | & |
按位XOR运算 | ^ |
按位OR运算 | | |
条件AND运算 | && |
条件OR运算 | || |
空合并运算符 | ?? |
条件运算符 | ?: |
赋值运算符和lambda | = += -= *= /= %= &= |= ^= <<= >>= >>>= => |
后置运算符 | x++ x--(出现在较长表达式中,在表达式计算完成后计算) |
对于基本运算符中x++和x--是出现在单独表达式中优先级高,等同于++x.
而表达式中有其他运算符则不参与表达式运算,表达式运算完成后再运算。
除了少数运算符外,所有的二元运算符都是从左到右运算。
类型的安全
类型转换
C#中支持两种转换方式: 隐式转换 和 显示转换。
隐式转换
只要能保证值不会发生变化,类型转换就可以自动的进行。
C#中支持的转换类型
源类型 | 目标类型 |
---|---|
sbyte | short int long float double decimal BigInteger |
byte | short ushort int uint long ulong float double decimal BigInteger |
short | int long float double decimal BigInteger |
ushort | int uint long ulong float double decimal BigInteger |
int | long float double decimal BigInteger |
uint | long ulong float double decimal BigInteger |
long ulong | float double decimal BigInteger |
float | double BigInteger |
char | ushort int uint long ulong float double decimal BigInteger |
注意,只能从较小的整数类型隐式的转换为较大的整数类型。
对于整数转换为浮点数之间转换可以在相同大小的类型之间相互转换,如int变float,long变double,但是也可以从float转换为long,尽管可能会丢失4个字节的数据,但是这仅仅表示float得到的值比double得到的值精度低。
无符号变量和有符号变量之间也可以相互转换,只要大小在范围之内。
在隐式转换值类型时,对于可空类型要考虑其他因素。
- 可空类型隐式的转换为其他可空类型同样需要遵循上面表的规则。
- 非可空类型隐式的转换为其他可空类型同样也需要遵循上面表的规则。
- 可空类型不能隐式的转换为非可空类型,此生必须显示的转换。
显示转换
不能进行隐式转换的一些场合:
- int转换为short 会丢失数据
- int转换为uint 会丢失数据
- uint转换为int 会丢失数据
- float转换为int 会丢失小数点后所有数据
- 任何数字类型转换为char 会丢失数据
- decimal转换为任何数字类型 因为decimal类型内部结构不同于整数和浮点数
- int?转换为int 可空类型的值可以是null
但是可以使用类型强制转换显示的执行这些转换,在把一种类型强制的转换为另一种类型时,有意迫使编译器进行转换,类型强制转换为一般语法如下:
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
long a = 100;
int b = (int)a;//使用括号对类型进行强制转换
WriteLine(b);//100
WriteLine(b.GetType());//System.Int32
ReadKey();
}
}
}
这表示把强制转换的目标类型名放在要装换值之前的圆括号里。
这种强制转换是一种比较危险的操作,即使是从long转换为int的简单操作,如果long的值大于int的最大存储值,就会出现溢出
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
long a = 77777777777777;
int b = (int)a;
WriteLine(a); 77777777777777
WriteLine(b);//215014513 这个值不一定
WriteLine(b.GetType());//System.Int32
ReadKey();
}
}
}
并且这种操作不会报错,难以察觉。
可以使用checked运算符检查类型转换是否安全,如果不安全,迫使编译器抛出一个溢出溢出。
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
long a = 77777777777777;
int b = checked((int)a);//抛出溢出异常
WriteLine(b);//
WriteLine(b.GetType());//System.Int32
ReadKey();
}
}
}
所有的显示类型转换都不安全,应该加入try和catch语句引入结构化异常处理。
使用类型强制转换可以把大多数基本类型从一种类型转换为另一种类型
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
double a = 4.3;
int b = (int)(a+0.69);
WriteLine(a);//4.3
WriteLine(b);//4
ReadKey();
}
}
}
这种显示转换为整数总是向0取整。取接近于接近于0的整数而不是四舍五入。
无符号整数转换为char会变成ASCII码字符。
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
int a = 43;
char b = (char)(a);
WriteLine(a);//43
WriteLine(b);//+
ReadKey();
}
}
}
这里将43变成了 + 。如果不是整数会先变成整数再转换为字符
值类型之间的转换不仅仅局限于孤立的变量,还可以将double的数组元素转换为int的结构成员变量,
using System;
using static System.Console;
namespace ConsoleApp16
{
struct A
{
public string Str;
public int X;
}
class Program
{
static void Main(string[] args)
{
double[] Price = { 20.5, 30.7, 1.2};
A a;
a.Str = "Hello World!";
a.X = (int)Price[0];
WriteLine(a.Str);//Hello World!
WriteLine(a.X);//20
ReadKey();
}
}
}
谨慎的使用显示的类型强制转换,就可以吧简单类型的任何实例转换为几乎任何类型,但是在显示的类型转换时有一些限制,就值类型来说,只能在数字,char类型和enum类型之间转换。不能直接的把布尔类型强制的转换为其他类型,也不能把其他类型强制转换为布尔类型。
如果需要在数字和字符串之间转换,就可以使用.NET类库中提供的一些方法。Object类实现了一个 ToString() 方法 。该方法在所有.NET预定义类型中重写,并返回对象的字符串表示:
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
int i = 10;
string a = i.ToString() + "asd";//数字变成字符串
WriteLine(a);//10asd
ReadKey();
}
}
}
同样,如果需要分析一个字符串,以检索一个数字或者布尔值,就看使用所有预定义类型都支持的 Parse() 方法:
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
string s = "100";
int i = int.Parse(s);//字符串变数字
WriteLine(i);//100
WriteLine(i + 777);//877
ReadKey();
}
}
}
注意如果不能转变为字符串,Parse()方法就会报错,如将asdw变成数字。
装箱和拆箱
装箱和拆箱可以把值类型转换为引用类型,并把引用类型强制转换为值类型。
装箱用于描述一个把值类型强制转换为引用类型。运行库会为堆上的对象创建一个临时的引用类型箱子。
该转换可以隐式的进行,还可以显示的转换。
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
string s = 10.ToString();//隐式
int a = 20;
object b = a;//显式
WriteLine(s);//10
WriteLine(a);//20
WriteLine(b);//20
WriteLine(s.GetType());//string
WriteLine(a.GetType());//int
WriteLine(b.GetType());//int
ReadKey();
}
}
}
拆箱用于描述相反的过程,其中以前装箱的值类型强制转换为值类型,
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
int a = 20;//值类型
object b = a;//引用类型
int c = (int)b;//装箱的以前的值类型变回值类型
ReadKey();
}
}
}
只能对以前装箱的变量进行拆箱,以前装箱的a,a是int类型,拆箱也必须是int。
在拆箱是应该注意值确保变量有足够的空间存储拆箱值中的所有字节。
比较对象的相等性
对象相等的机制不同取决于比较对象是引用类型还是值类型
比较引用类型的相等性
System.Object定义了三个不同的方法来比较对象的相等性,ReferenceEquals() 和两个版本的 Equals() 。再加上比较运算符 ==,实际上一共有四种方法比较相等性。这些方法有一些细微的区别,
ReferenceEquals()方法
是一个静态方法,其测试两个引用是否指向类的同一个实例,特别是两个引用是否包含内存中的相同地址。作为静态方法,不能重写。
因为比较的是内存地址,所以对于值类型总是为false。
如果提供的两个引用指向同一个实例对象,返回 true ,否则返回 false。
但是它会认为 null 等于 null
using System;
using static System.Console;
namespace ConsoleApp16
{
class Program
{
static void Main(string[] args)
{
int a = 10;//整形a
object b = a;//b对a装箱,b是引用类型
object c = b;//将b等于c
object d = 10;//创建引用类型d同样等于10
bool e = ReferenceEquals(b,c);//b和c都是a的引用
bool f = ReferenceEquals(b,d);//引用指向值都是10,但不是同一个10
WriteLine(e);//true
WriteLine(f);//false
WriteLine(ReferenceEquals(null,null));//true
WriteLine(ReferenceEquals(b,null));//false
ReadKey();
}
}
}
Equals()虚方法
也是可以比较引用,但这个是虚方法,因此可以在自己的类中重写,从而按值比较对象。
特别是如果希望类的实例用作字典中的键,就需要重写这个方法。
重写 Equals() 方法是要注意,重写的代码不应该跑出异常。
静态的Equals()方法
Equals() 的静态版本与其虚实例方法版本作用相同,其区别是静态版本带有两个参数,并对它们进行相等性比较。
这个方法可以处理有两个对象中有一个是null的情况,当有一个参数时null时,就会抛出异常。
使用静态 Equals() 方法时首先检查传给它的引用是否为null,如果他们都是null,同样返回true。如果只有一个null,就返回false。
如果两个引用