c#笔记-运算符

一元运算符

数字运算

正负

在数字前面,或数字类型的变量前面使用正负号,可以表示这个数值取自己,或是取相反数。

int i1 = +3;
int i2 = -3;
int i3 = +i2;//-3
int i4 = -i2;//3

自增

一个数字类型在自己前面或后面连写两个+-,可以表示自增/自减

int i5 = 10;
i5++;
Console.WriteLine(i5);
i5--;
Console.WriteLine(i5);

写在前面和后面的区别是,如果是一个复杂的表达式中,同时需要对他们取值,
那么写在前面的就会得到运算结算后的值,写在后面就会先取值再运算。

也就是说如果不取值,只是单独作为+1使用,用哪种结果都一样。不过++放在前面效率更高。

int i6 = 10;
Console.WriteLine(i6++);//输出10。但是计算完成后就会变成11
Console.WriteLine(i6);//输出11,来自刚刚的自增
Console.WriteLine(++i6);//立刻自增为12,并把12输出
Console.WriteLine(i6);//输出12,没有变化。

覆盖原值的时机,是在执行完一个++--后立刻执行,而不是等它所在的整个表达式执行完毕。

int i7 = 0;
int i8 = (i7++) + (i7++) + (++i7) + (++i7);
//取值时,值依次是:0,1,3,4
//取值完毕后,值依次是:1,2,3,4
Console.WriteLine(i8);

这种在表达式里写多个,甚至是同一个变量的自增/自减只会在考题中出现。
在日常代码中不要这样写。

逻辑运算

逻辑非

对于任意的bool值,前面加上!,可以表达相反的bool值。

bool b1 = true;
bool b2 = !b1;//false
bool b3 = !b2;//true

位运算

取反

对于整数类型,在前面加上~,会反转所有比特。

int i9 = ~4;
Console.WriteLine(i9);//-5

真实的数字,正数和负数是对于0对称的,0刚好处于分界线上。
但是对于计算机的储存方式来说,0只有一个值,只需要把他放在一边就好。
所以正数和负数不是对称的,因此他们会差1.

位运算是直接对储存数字的比特进行运算。
所得到的结果都是二进制下的运算,对我们来说是不直观的。
你可以理解为,一旦使用了位运算,就是不在乎它表达的数学含义了。
而是对他进行压缩或解压缩。

例如一个bool类型,占1字节(8比特)。但他只储存一个比特的数据,有7个比特都是浪费的。
但是如果对int类型使用位运算,那么4字节的数据就能实实在在储存32个bool数据。

二元运算符

数学运算

基本的加减乘除都有。分别使用+,-,*,/作为运算符。
需要强调的是,除了这4种,还有一个取余运算符%
在一个除法的计算中会被迫的把余数求出来,所以余数被作为了一个和除法同级的运算。

在一个复杂的表达式中,数学运算同样遵循先乘除(和取余),后加减,
有括号先括号,从左到右依次计算的运算顺序。

int i10 = 3 + 6 * (4 - 2) % 5 - 2;//先计算4-2=2。然后计算6*2%5=12%5=2。最后计算3+2-2=3
Console.WriteLine(i10);//3

整数的除法不会保留余数,不会四舍五入,会完全舍去。

关系运算

关系运算一共有6种,分别是大于,小于,不大于,不小于,等于,不等于。
符号分别是>,<,<=,>=,==,!=所有包含=号的运算符,=号都在右边。

一个=号永远是赋值语句。等于的概念使用两个=号。

bool b4 = false;
if (b4 = 1 > 2)//这是把1<2(false)的结果作为判断条件,而不是判断b4(false)是否和1>2(false)相等
{
     
     
	Console.WriteLine(true);
}
else
{
     
     
	Console.WriteLine(false);
}

位运算

逻辑位运算

逻辑位运算有3种,异或,符号分别是&,|,^
和逻辑运算不同的是,位运算无论左侧值如何,都会把右侧值进行计算。
而且,他们可以对整数类型使用,效果是对所有比特进行这种逻辑的位运算。

int i12 = 0;
if (i12 == 0 | 10 / i11 > 2)//如果i12值为0则报错。
{
    
    
	Console.WriteLine("通过");
}

异或是指,如果两侧的值(或)不一样(异),则为true,如果一样则为false。

int year = 2400;
bool b5 = year % 4 == 0 ^ year % 100 == 0 ^ year % 400 == 0;
if (b5)
{
    
    
	Console.WriteLine(year + "是闰年");
}
else
{
    
    
	Console.WriteLine(year + "不是闰年");
}

异或有一些运算性质。例如

  • A和false异或后,值为A。(异或false,值不变)
  • A异或A,值为false。(自己异或自己一定是false)
  • A异或B异或B,等同于B异或B异或A,值为A。(异或满足交换律)
  • 一长串值进行异或,结果值为:参与运算的值中,true的数量为奇数。
    (两个true会抵消成一个false。任何值异或false,值不变)
    (上面例子的闰年,就是要求这3个条件满足1个或3个的时候才是闰年)

一个对数字位运算的例子,一个uint刚好可以用来储存8位rgba颜色。
使用位运算可以摘出每个通道的值。

uint rgba = 0xf2c6b800;
uint r = rgba & 0xff000000;
uint g = rgba & 0x00ff0000;
uint b = rgba & 0x0000ff00;
uint a = rgba & 0x000000ff;

if (r == 0xf2000000)
{
    
    
	Console.WriteLine("红色值是f2");
}

rgba颜色格式的每个字节分别表示

  • r:red,红色
  • g:green,绿色
  • b:blue,蓝色
  • a:Alpha,透明度

使用&运算时,对1位置(一个f有4个1)会保留值,对0位置会排除值。
所以执行了&运算后,只有需要的地方会留下来。不需要的地方会排除。
然后再进行比较,就可以排除掉干扰项。正如一开始说的,位运算是压缩/加密/解压/解密的操作。
这4个互不干扰的值理应分别存在于4个变量中。他们在一个变量里其实就是一个压缩的值。
而现在的排除干扰就是在解压缩。

位移运算

整数类型有位移运算,把所有比特向一个方向移动。
导致的效果和直接的*2,/2一样。位移的效率比计算*2,/2要高,但局限性太大。
要么是底层代码做效率优化使用,要么还是加密解密使用。
左位移的符号是<<,相当于*2(的n次方)。
右位移的符号是>>,相当于/2(的n次方)。

uint rgba0 = 0xf2c6b800;
byte r0 = (byte)(rgba0 >> 6);
byte g0 = (byte)(rgba0 >> 4);
byte b0 = (byte)(rgba0 >> 2);
byte a0 = (byte)(rgba0 >> 0);

位移会无视所有超出边界的值(舍弃掉),而补进来的值都是0。
加上从uint到byte的转换会截断左侧的值。
所以之前的颜色也可以使用这样的方式来摘出。

特殊表达式

三元运算

三元运算有一个条件,和两个候选项。
如果条件为true,则获得冒号左侧的值,如果为false,则获得右侧的值。

int i12 = 8;
var s1 = i12 % 2 == 0 ? "偶数" : "奇数";
Console.WriteLine(s1);
Console.WriteLine(i12 % 2 == 0 ? "偶数" : "奇数");

三元运算很类似if-else语句,不过它只是一个,不是语句,不能独立放置。

int i13 = 0;
true ? i13++ : i13--;//错误,不能独立成句。
i13 = i13 + (true ? 1 : -1);//正确。记得打括号

三元运算符也可以嵌套出多个if-else的类似语句。

int i14 = Random.Shared.Next(100);
var s2 = i14 > 98 ? "评分SS"
	: i14 > 95 ? "评分S"
	: i14 > 90 ? "评分A"
	: i14 > 80 ? "评分B"
	: "不通过";
Console.WriteLine(s2);

三元运算符也可以往冒号的左边嵌套。但是太复杂了很难看。

三元运算只有需要执行的一侧值会被执行。而不是在计算前先算出所有候选值。

int i15 = 10;
int i16 = true ? ++i15 : --i15;
Console.WriteLine(i15);//如果同时执行两边,那么值就不会变

switch表达式

类似三元运算符是对需要取值的if-else简写。switch表达式是对需要取值的switch选择简写。
同样,switch表达式

  • 只会执行需要取值的地方
  • 有执行顺序
  • 不能独立成句。
int i17 = Random.Shared.Next(100);
var s3 = i17 switch
{
    
    
	> 98 => "评分SS",
	> 95 => "评分S",
	> 90 => "评分A",
	> 80 => "评分B",
	_ => "不通过"
};
Console.WriteLine(s3);

=>长得很像一个箭头。所以说,关系运算符的=都在右边。因为=在左边被这里征用了。

和switch选择语句一样,主判断只能使用模式匹配判断,如果要变量参与则要用when打开次要判断。

int i18 = Random.Shared.Next(100);
var s4 = true switch
{
    
    
	true when i18 > 98 => "评分SS",
	true when i18 > 95 => "评分S",
	true when i18 > 90 => "评分A",
	true when i18 > 80 => "评分B",
	_ => "不通过"
};
Console.WriteLine(s4);

_表示舍弃,即不再进行任何判断,是保底的默认分支。
默认分支也可以使用when进行次要判断。
但最好保留一个无条件的默认分支,否则编译器给你加的默认分支只会获取默认值。

逻辑短路运算

逻辑运算也是一种复合运算,不能够自定义。
逻辑运算有两种,与(且),分别用&&||符号。

  • x && y相当于!x ? x : x & y
  • x || y 相当于x ? x : x | y

他的含义是,如果左侧能直接得出结果,那就直接把左侧值返回,否则才计算位运算。
例如,整数的除法不能用0作为除数。左侧为true时,||能直接得出结果,不会再计算10 / i11 > 2

int i11 = 0;
if (i11 == 0 || 10 / i11 > 2)//不要写成if (i11 == 0 )|| 10 / i11 > 2
{
    
    
	Console.WriteLine("通过");
}

自赋值运算语句

所有的数学运算,位运算,都可以使用复合语句。
A = A [运算符] B可以简写为A[运算符]= B

for (int i = 0; i < 20; i += 2)
{
    
    
    Console.WriteLine(i);
}

由于异或运算的特性,把一个bool值取反赋值可以使用自异或赋值。

bool qwertyuiop = true;
qwertyuiop ^= true;

这个技巧可以在变量命过长的时候使用。无论变量名多长,右侧都只要写true这4个字母。
qwertyuiop = !qwertyuiop方式需要把变量命写两遍。

逻辑运算符不支持自赋值运算。
而关系运算得到的值(bool)都不是计算值的类型(数字)
但位移运算可以使用自赋值运算。

多赋值语句

在同一个语句中,可以连续对多个变量进行赋值。

bool b6, b7, b8, b9;
b6 = b7 = b8 = b9 = true;

这个的执行顺序是从右往左,即等同于

b9 = true;
b8 = true;
b7 = true;
b6 = true;

等效赋值是使用最右侧的值true进行赋值,而不是各自的右侧的true,b9,b8,b7依次赋值。

无论在这个赋值过程中,这些变量发生了怎样的变化,都不会影响其他变量的赋值。
例如说假设有一种操作,在一个变量接收赋值后会立刻把自己乘2。
那么依次赋值就会得到乘2后的值。而同时赋值则不会影响。

一个赋值语句也可以(用括号)插入到一个表达式中。同样的,取值会以给他赋值的值为准,
无论这个变量接收赋值后的值如何,或者赋值以前的值如何,都不会影响。
但如果使用的是自赋值语句,那么取得得值就是变量赋值后的值(如果它因为赋值触发操作,会得到操作后的值)。

int i19 = 0;
int i20 = (i19 = 20) + i19 / (i19 += 5) - i19;

插值字符串

在字符串前加上$修饰,可以将字符串改为插值字符串格式。
插值字符串可以接收一对大括号的占位符,里面使用程序计算的值。

string name1 = "小明";
int age1 = 12;
string hello1 = $"名字是:{
      
      name1},年龄是{
      
      age1}";
Console.WriteLine(hello1);

字符串是不能在原值上修改的。每次字符串发生变化都是产生了新的字符串。
+是依次计算的,所以"名字是:" + name1 + "年龄是" + age1会依次产生

  1. 名字是:小明
  2. 名字是:小明,年龄是
  3. 名字是:小明,年龄是12

其中的1和2都是没有用的中间产物,而插值字符串会考虑到所有的值后,再对字符串合并,
因此不会产生中间产物,具有更高的效率。而且也更容易看。

原始插值字符串

原始字符串是为了避免转义而设计的。而插值字符串中的占位符{ }也是一种转义符。
同样的为了避免复制的时候转义,可以在原始字符串前加任意数量的$进行插值字符串转义。
在字符串中,需要使用同等数量的连续{ }来启用和结束占位。

string name2 = "小明";
int age2 = 12;
Console.WriteLine($$$"""名字是:{
    
    {
    
    {
    
    name1}}},年龄是{
    
    {
    
    {
    
    age1}}}""");

空格占位

在插值后,可以接一个逗号和一个常量,表示这个值至少要占多少个字符。
如果没有达到指定数量,就会用空格补齐。达到或超过则无事发生。

Console.WriteLine($"123456789012345678901234567890");
Console.WriteLine($"12|{
      
      456,6}0|23456789|{
      
      123,-7}|90");

如果是正数,空格会补在前面,如果是负数,空格会补在后面。上述输出为

123456789012345678901234567890
12|   4560|23456789|123    |90

格式说明符

在插值后(或空格占位后)可以加上冒号来描述这个类型的格式说明。
格式说明对每个类型来说都是特定组合。所以也有很多类型不能使用格式说明。

double d = 12.123456;
int i21 = 285;
Console.WriteLine($"{
      
      i21:d5}");//00285,以前导0补足指定数量的数字
Console.WriteLine($"{
      
      i21:x}");//11d,以16进制显示。字母部分的大小写取决于说明符的大小写
Console.WriteLine($"{
      
      d:f3}");//12.123,指定小数位数,会四舍五入或补0来达到指定位数
Console.WriteLine($"{
      
      d:e2}");//1.21e+001,科学计数法显示,e后面的数字即小数点后的有效数字,四舍五入

其他说明符参阅此页

格式说明符的冒号和三元运算符的冒号是有歧义的。
如果想在插值中使用三元运算,则必须对其打括号。

Console.WriteLine($"{
       
       (true ? 10 : 20),12:d5}");

其他

运算顺序

详细介绍参阅此页
总结下来内容如下

  1. 内容访问。例如int.MaxValue,所有带点的,取的值都是点右侧的值。
  2. 一元运算。所有一元运算都是对这个值本身进行修饰。需要先修饰后才和其他值计算。
  3. 范围运算。是一个构造值的特殊语法。
  4. switch表达式。虽然和三元运算符类似,但他们的优先级差了很多。
  5. 数学运算
    5.1 乘法,除法,取余
    5.2 加法,减法
    5.3 位移运算
  6. 关系运算
    6.1 大于,小于,不大于,不小于
    6.2 等于,不等于
  7. 逻辑运算
    7.1 位与
    7.2 位异或
    7.3 位或
    7.4 逻辑与
    7.5 逻辑或
  8. 三元运算
  9. 赋值

以下示例可以演示逻辑运算优先级。

Console.WriteLine(true || true && false);   //True
Console.WriteLine((true || true) && false); //False

舍弃

在赋值语句中,可以使用下划线_来作为接收结果的赋值语句。
下划线表示舍弃,这样做会计算右侧的值,可以帮助一些不能独立成句但有操作的语句。

int i22 = 10;
_ = i22 % 2 == 0 ? i22 /= 2 : i22++;

下划线是有效的变量命。如果在这条语句的可见范围内声明了一个名为下划线的变量,
就无法进行此种操作。即便这个语句不在下划线变量的作用域内。

不同类型的运算

所有的类型,在使用运算符时如何进行运算,都是预先写好的固定操作。
也包括了和不同的类型计算时,会得到什么样的类型。

  • 数字类型
    • 类型不同时,以范围更大的类型为准。
      • 整数和整数运算时,以范围更大的整数。
      • 浮点数和浮点数运算,以范围更大的浮点数
      • 整数和浮点数运算,任何浮点数的范围都超过任何整数类型
    • 所有的二元运算最少也会以int为基准。即便是同类型运算(例如byte和byte)也会得到int。
  • 字符串类型
    • 字符串类型可以和任何类型进行相加。会把对方也变成字符串然后做拼接。
  • 其他
    • 其他类型需要定义了运算,才能进行运算。没写的是不让你算的。

类型变动是对每一个运算的左右两边依次判定,而不是在一开始分析整个表达式做出决定。例如

//先由一堆整数进行除法操作。 最后与一个浮点数运算。这样也不会保留之前的余数。
Console.WriteLine(90 / 20 / 3 / 2.0);//0.5
Console.WriteLine(90 * 1.0 / 20 / 3 / 2.0);//0.75
//几个数字类型进行计算,然后和字符串相加,得到的结果是数学运算后的结果。
Console.WriteLine(1 + 1 + 1 + 1 + "1");//41
Console.WriteLine(1 + 1 + "1" + (1 + 1));//212
Console.WriteLine(1 + 1 + "1" + 1 + 1);//2111

猜你喜欢

转载自blog.csdn.net/zms9110750/article/details/130478675