说点奇怪的。
C# 出了一个叫做 ref struct
的机制,它使得一个对象只可以放在栈内存里(注意是“只可以”,和 struct
不一样,struct
可以因为装箱机制存到堆内存里)。这个 ref struct
由于是在栈内存里分配的连续空间,所以更类似于 C 语言最纯粹的数组。
ref struct
可以由自己实现,不过官方给了一个东西叫 Span<T>
。为了引入下面的内容,我们先给出一个 stackalloc
这个指针才用得上的语法:
unsafe { // 栈内分配三个连续 int 内存块,并反馈这个内存块的首地址,所以 ptr 是一个指针。 var ptr = stackalloc int[3]; }
后来有了 Span<T>
后就可以这么写了:
Span<int> span = stackalloc int[3]; // 隐式转换。 var span = (Span<int>)stackalloc int[3]; // 因为机制的问题,必须指定一种转换。 // 否则 span 还是被看成指针。
可是有个骚操作是
var span = (stackalloc int[3]); // Span<int> 类型。
你只需要加一个小括号在 stackalloc
运算符表达式的两侧,span
就自己成 Span<int>
类型了。
条件运算符从来不能放在赋值号的左侧,不过我们可以用 ref
来做到这一点。
int a = 2, b = 3, c = default, d = default; (a > b ? ref c : ref d) = 1;
这样,你说到底是 c
赋值为 1,还是 d
赋值为 1 了呢?
我尝试了 SharpLab,不过老实说这个写法确实有点奇妙。在编译器生成的代码里,第二行的代码实际上被变成了这样:
(a > b) ? ref c : ref d = 1;
但我们手写这一行代码,却会引起编译器错误,提示语法不对(括号没写对位置)。这就很骚了。
不过,官方给的解释是,条件表达式在 C# 7 里才引入可以带 ref
的情况,所以写法其实应该是这样的:
ref int a = ref (x > y ? ref x : ref y);
注意自己看,右侧的条件表达式有三个 ref
,其中后面两个 ref
表示取的 x
和 y
应以引用传递,而不是数值传递;而为什么 x > y
条件左边还得要一个 ref
呢?很简单,因为左侧是 ref
的局部变量,所以为了配套语法所以才需要配对出现 ref
的,所以右边有三个 ref
。
所以小括号应该把表达式括起来。至于为什么编译器生成的代码括号在条件这一点,始终说不清楚。
另外,ref
这个关键字除了可以用在这里,还可以用到返回值上,指示函数的返回值是带引用的。如果我们这样干了,调用的时候就可以把函数调用作为左值使用。在官方的 Unsafe
类里,有个 As
函数,这个函数可以以不拆箱的形式对引用类型和值类型之间做转换:
Unsafe.As<object, int>(ref a) = 25;
由于它的返回值是 ref
的,所以你可以这么用。
不过这个 Unsafe
类有点神奇的是,它并不是 C# 写的,而用的是 IL 直接写的。你敢信?!如果你想看代码:
/// <summary> /// Reinterprets the given reference as a reference to a value of type <typeparamref name="TTo"/>. /// </summary> [Intrinsic] [NonVersionable] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TTo As<TFrom, TTo>(ref TFrom source) { throw new PlatformNotSupportedException(); // ldarg.0 // ret }
你就会发现,你根本看不到源代码,而是抛异常。真正的代码其实是被注释的这一坨东西,也就是 IL 代码了。
??=
(C# 8)经常我们要写这个代码:
if (a is null) { a = b; }
不过现在可以简化成 a ??= b
了。这是因为语法使得这个写法等价于 a = a ?? b
,当 a
为 null
时候,会得到 b
的值作为赋值结果;否则不变。
true switch
表达式(C# 8)switch
表达式是 C# 8 提出的一种概念,把 switch
一大块语句变为一个表达式,这样可以直接拿来赋值。
不过,你可以使用 true switch
表达式,来对一组条件挨个判断和赋值:
int b = 36; int a = true switch { _ when b < 0 => 1, _ when b < 10 => 2, _ when b < 30 => 3, _ when b < 60 => 4, _ => 5 };
当然,这个是搞笑的,因为其中的 true
可以替换为任意变量。反正你下面的表达式里也没用到。
true
和 false
运算符这个是最初 C# 就有的机制。叫 true
和 false
运算符,我感觉有一点容易和他们自己本身的用法混淆,所以我更喜欢叫他“布尔算子”(Boolean Functor)。注意这个名字是我自己取的,不要当成术语词汇了。
这个运算符是专门用来重载 &&
和 ||
这两个看起来根本没办法重载的运算符的。不过并不是说 &&
就对应 true
,而 ||
就对应 false
。这个说法是错的。实际上,重载运算符的语法是这样:
public static bool operator true(Instance a) { // ... } public static bool operator false(Instance a) { // ... }
而实际上,你只重载了他们还不行,你还需要重载三个运算符(其中有一个是可选):&
、|
和 ^
。说对了,^
是可以不重载的。因为它可能有别的用,但最好配合逻辑运算符还是一起重载了吧。
如果你要表达一个自己定义的对象,使得可以使用 a && b
和 a || b
的话,需要照着这个公式来:
a && b
等价于 false a ? a : a & b
;a || b
等价于 true a ? a : a | b
。所以,只要你照着这个写法来重载上面说的 true
、false
、&
和 |
后,就可以使用 &&
运算符和 ||
运算符对自己自定义的对象了。一定要注意,false
算子配套的是 &
而不是 |
。
我们有时候会这么用到代码:
var map = new Map(); map.A = ...; map.B = ...;
可是后来有了初始化器之后,语法就变得简单了:
var map = new Map { A = ..., B = ... };
这样确实带来了很多方便的地方。不过还是需要这里的 A
和 B
是一个同时有 get
和 set
的属性才行。
不过,如果一个对象并不是 new
出来的,而是其它的对象了呢?
var map = tempMap;
在此基础上我们想要给 map
新添加一些内容,这怎么办呢?初始化器语法是不支持这样的。
换个思路,我们去写一个构造器,让这个类(结构)支持自己类型的参数进行初始化:
public Map(Map another) { // ... }
接着,你就可以用这个构造器来初始化对象了:
var map = new Map(tempMap) { A = ..., B = ... };
自从弃元(Discard)这玩意儿出来了之后,就有各种高端玩法。比如下面这种:
int a = 3, b; b = _ = a; // What the hell...
弃元是可以不声明就可以书写的一个特殊变量,所以你可以放在两个变量赋值之间。甚至……
int a = 3, b; b = _ = _ = _ = _ = _ = a;
这个也是用来搞笑的。
void
类型的方法,可以不在所有路径返回有些时候,我们不得不使用死循环。死循环是一种无法跳出循环体的一种循环,比如下面这样的方法,带了一个死循环:
static int GetValue() { while (true) { // ... return 3; } // Here does not need any return clause. }
比如这样的组合,因为死循环无法跳出,所以我们无需在 while (true)
外部添加任何的返回语句,因为是无法执行到那里去的,除非死循环里有 break
,不过这样已经不是死循环了。
null
也不报错什么?null
什么时候用都要异常的啊。实际上不是。比如这个例子:
public static implicit operator object(Hell hell) => new object();
假设我们的 Hell
类型是一个引用类型,然后你就可以优雅的使用这个隐式转换对 Hell
类型作为转换了:
object o = (Hell)null; o.ToString();
呃,这样不会报错,你敢信?
__arglist
关键字兼容早期编程语言的可变长参数序列这个不必多说了,大概是这么用的:
[DllImport(...)] static int printf(string format, __arglist);
我们考虑简单一些的情况。翻转一个 bool
变量,我们应该怎么写呢:
static void Flip(this ref bool @this) { @this = !@this; }
应该就可以了。这样调用的方式和之前调用方法的模式完全一样,不过这样的写法可以影响到 @this
变量本身,所以不需要写其他的东西:variable.Flip()
。
基本上看SO的这个问题就够了:
tips and tricks给__arglist等关键字添个示例吧:
class Program { [DllImport("msvcrt.dll")] public static extern int printf(string format, __arglist); [DllImport("msvcrt.dll")] public static extern int scanf(string format, __arglist); public static void WriteTypes(__arglist) { ArgIterator ai = new ArgIterator(__arglist); while(ai.GetRemainingCount() >0) { TypedReference tr = ai.GetNextArg(); Console.WriteLine("Type:{0}", __reftype(tr)); Console.WriteLine("Value:{0}", __refvalue(tr, int)); } } static void Main(string[] args) { int x, y; scanf("%d%d", __arglist(out x, out y)); printf("hello %d
hello %x
", __arglist(x, y)); WriteTypes(__arglist(x, y)); } }
SIMD支持
居然没有提到最新的SIMD指令支持啊,这个貌似碉堡:
// 两个整数数组相加的常规方法 for (int i = 0; i < size; i++) { C[i] = A[i] + B[i]; } // SIMD 方法, 每次几个元素同时相加,Vector<int>.Count是每个SSE/AVX寄存器容纳int的个数 using System.Numerics.Vectors; for (int i = 0; i < size; i += Vector<int>.Count) { Vector<int> v = new Vector<int>(A,i) + new Vector<int>(B,i); v.CopyTo(C,i); }
更碉堡的是,Vector方法是硬件自适应的。也就是说,如果你的硬件只支持SSE2,就每四个int相加,如果支持AVX2,就每8个相加。
无GC模式
调用GC.TryStartNoGCRegion(int64)函数,传入一个内存大小(比如1G)。CLR会开辟一个指定大小的内存区域,然后进入无GC模式。适用于critical path部分的业务逻辑。
1、泛型类型字典,这个已经基本不算奇技淫巧了,因为大家都在用:
private static class ConverterCache<T> { public static IDbValueConverter<T> ConverterInstance { get; set; } }
上面是随便从我一个项目里面摘出来的最简单的类型字典的例子,一个最简单的类型字典只需要一个泛型类,和一个静态字段就够了。
详细请参考:
泛型技巧系列:类型字典和Type Traits2、利用using来做Scope,其实我个人不是很喜欢这个技巧,在
http:// ASP.NETMVC里面广泛使用:
using( Html.BeginForm() ) { //... }
3、其实扩展方法可以做很多好玩的东西:
public static T CastTo<T>( this object obj ) { return (T) obj; }
public static T IfNull<T>( this T value, T defaultValue ) { if ( value == null || Convert.IsDBNull( value ) ) return defaultValue; else return value; }
我还有一个扩展方法把一个类型所有属性和属性值转换成一个Dictionary的,代码就不贴了,除了一些常规用途,有时候初始化一个Dictionary很麻烦的时候,还可以直接new一个匿名对象,再用这个扩展方法转成Dictionary就完了。
4、运算符重载
运算符重载理论上不算什么奇技淫巧,是个标准特性,但不知道为什么用的人特别少。
var logger = new ConsoleLogger() + new TextFileLogger( @"C:TempLogs1.log" );
我的LogUtility,可以将多个Logger用+连接起来,一并记录日志。
5、dynamic绑定
dynamic说白了就是运行时绑定,而且绑定到什么逻辑上是可以在运行时再根据上下文重新定义的。劣势是没有智能提示,但有些地方根本不需要智能提示,或者没有强类型,例如数据绑定。
我们现在的页面数据绑定已经开始倾向于大量使用dynamic,这样一来页面数据绑定便可以先行,根本用不着更新组件接口神马的,需要什么东西直接绑,反正不到运行时不会检测这个东西是不是存在。
而运行时真正执行这个绑定的时候,因为是代码逻辑,所以可以玩出很多花样。
最简单的花样像是大小写不敏感,绑定name属性,找到了一个叫做Name的属性,把值给他就好了。
复杂一点的花样,例如找不到Name属性,就输出一个字符串"[Name]"然后页面上就看到:
姓名:[Name]
这样一来,前端页面原型完全可以演示,根本不用等后面的数据对接,啥时候对接完了,自然变成最终的数据。
再复杂一点的,譬如有个属性改名了,原来的名字不存在了,没事,映射到新的名字,维护一个改名兼容表就完了。
其他的想到再补充。
我觉得 泛基是一种很棒的技巧 这个名字是和 @赵劼 一同起的。
public abstract class MyBase<T> where T :MyBase <T> //被人肉编译器 @vczh 发现少写了<T> { public static string DataForThisType {get;set;} public static T Instance{get;protected set;} //老赵说的邪恶的部分:让父类操作子类出现了 public static readonly IReadOnlyDictionary<string,MemberInfo> Members =typeof(T) .GetMembers() .ToDictionary(x=>x.Name); } public class MyClass:MyBase<MyClass> { static MyClass() { DataForThisType =string.Format( "MyClass got {0} members",Members.Count); Instance = new MyClass(); } }
子类的static 成员互相不干涉
@赵劼
简单说就是让父类可以静态化地知道当前子类…
------------
补充用途:
1 你有时候希望在父类规定一些行为,让子类无法修改,但是这些实现是依赖一个子类才能获取的值,你又不可能知道所有的子类 ,没办法替它在父类里面初始化,这时候就需要在父类里面定义一个每个子类一个的,但又是静态的空间。
2 你需要每个子类都有一些公开的静态成员,这些成员的类型是子类类型
3 在不知道子类具体类型的情况下,让父类利用泛型参数先替未来的子类做一些事情。
-----
貌似能实现这样功能的其他语言不多。 可是我没有挨个试验过 有人能有反馈告诉我自己熟悉的语言能否做到的话,感激不尽。