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
会依次产生
名字是:小明
名字是:小明,年龄是
名字是:小明,年龄是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}");
其他
运算顺序
详细介绍参阅此页。
总结下来内容如下
- 内容访问。例如
int.MaxValue
,所有带点的,取的值都是点右侧的值。 - 一元运算。所有一元运算都是对这个值本身进行修饰。需要先修饰后才和其他值计算。
- 范围运算。是一个构造值的特殊语法。
- switch表达式。虽然和三元运算符类似,但他们的优先级差了很多。
- 数学运算
5.1 乘法,除法,取余
5.2 加法,减法
5.3 位移运算 - 关系运算
6.1 大于,小于,不大于,不小于
6.2 等于,不等于 - 逻辑运算
7.1 位与
7.2 位异或
7.3 位或
7.4 逻辑与
7.5 逻辑或 - 三元运算
- 赋值
以下示例可以演示逻辑运算优先级。
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