我写的C#多于Java,也更喜欢C#。
我一个同学说过“没有JDK文档的情况下,我没法写Java,但是没有MSDN的情况下,我照样写.Net的程序。”这话我十分赞同。因为Java太混乱了,很多不够合理的地方,没有文档的话,很多你想找的类,你不知道该去哪个包里找。而C#更接近于人的思维习惯。
说一个我跟别人说过很多次的例子。
---
你要在代码中获取系统当前时间。你会去哪里找?至少我的第一想法是去Date类、Time类或者DateTime类里找。我在Java里找了好久,最后发现在Calendar类里。。。
而且Java下,这个从Calendar类获取的系统时间,要转化成Date、Time之类的东西才能在别处用,可是,印象中要从Date、Time类型的变量中提取int类型的小时、分钟神马的值,很麻烦,反而获取从19xx年1月1号开始的毫秒数很容易。但问题是,我获得这玩意之后可以干嘛?用它比较时间先后倒是不错。
但是在C#下,你直接去DateTime类里,就能找到。DateTime类里有个属性叫Now。而且C#的DateTime类,可以很方便的获取小时、分钟什么的。
---
而且C#中可以重载运算符,直接用大于号、小于号就可以比较时间先后,用==就可以比较字符串。
而Java里比较字符串非得用.equals(),很纠结呀。
对于那些不习惯用==比较字符串的Java程序员,你在C#里也仍然可以使用.equals()。
---
记得有人跟我讲过这样一个事:微软专门请过1000个程序员,给他们n小时,写一个读写文件的程序。结束之后,经过微软统计发现,这1000人里大部分人首先想到去找File这个类。于是微软就在它的C#里,把File做成了个静态类,专门提供各种用于读写文件的方法。
---
泛型这东西是C#首先支持的。泛型这东西,可以大大降低强制转换的次数,降低错误转换的可能性。而Java似乎在JavaSE1.5(有可能是1.6,记不清了)才开始支持泛型。所以你在写Java的时候,有些第三方jar包,为了兼容这之前的java版本,里面的方法给你返回的是object类型的东西,需要你手动转换。
---
C#下的枚举也比Java安全。Java里面的枚举直接用int强制转换过去就行,你甚至可以在不知道一个int在枚举中意味着什么,就把它作为参数传进去。而C#里的枚举是强类型,不能随意转换。
---
C#下有很多用来降低程序员因为马虎出错的可能性的东西。
比如参数的ref、in、out标记。
加了in标记的参数,你在函数里就没法对它进行赋值了。防止程序员错误的向变量写入东西,降低bug率。而且在后期维护程序的时候,负责维护的程序员看到in标记之后,就知道这个函数无论怎么改,这个参数不要动。
而加了out标记的参数,系统就会允许调用者传未赋值的变量进来。但是,不管这个参数传进来的时候有没有被赋值,系统会要求你首先对这个变量进行赋值才能使用。就是说,变量里任何已经存在的值你是没法使用的,必须要覆盖掉。而且如果方法里存在一个代码路径没有对这个参数进行赋值,系统会报错。这个也可以降低bug数量。
加了ref标记的参数,对于引用类型的东西,比如类的实例,这个ref标记加不加没有区别。但是对于值类型的参数,比如int、结构体等,就可以以传引用的方式调用了。
---
C#下,集合操作远远比Java方便。尤其是有了泛型。
C#下,几乎所有集合类型都可以转换成IEnumerable<T>类型,IEnumerable<T>可以用ToList()方法转换为List<T>,而且都可以用foreach来遍历。Java里,List是抽象类,我最初接触Java的时候,找了好久都不知道为什么new List()会出错。。。最后才知道要new HashSet()
C#下的Linq、Lambda表达式可以很方便的对集合查询。
---
C#的好多集合类型、Collection类型,都可以用索引器。比如List<String>类型的strList,我们可以直接strList[i]来获取第i个元素。Dictionary<String,String>类型的someDict,我们可以直接用someDict[someKey]来获取某Key对应的Value。
---
C#里的属性是个化繁为简的好东西。
Java里为了写一个JavaBean,你需要为每个属性写一个千篇一律的getXXX()和setXXX方法,而在C#里你可以简单的写String someProperty{get;set;}就行。get和set也可以单独限定private、public等限定符。也可以自定义get和set访问器,以便在需要的时候进行一些关联操作。比如在set的时候可以调用OnSet之类的事件委托。
---
委托,这个也是一个极好的特性。
记得写Java的桌面应用的时候,一个类要实现MouseListener接口,然后在处理函数里判断
if(被单击的是Button1)
{/*Do something*/}
else if(被单击的事Button2)
{/*Do something*/}
......
如果窗口上的button多一些,这个函数可能要几百行,而且代码乱的不得了。但是如果是C#,可以为每个button的click事件指定不同的处理函数,代码清晰简洁。
在IoC的实践中,C#的委托也比Java的传接口的实例更为有优势。
---
C#下,反射比Java更易用。
而且C#里的MEF也是个不错的东西。我们团队之前用MEF开发了一个东西,每个模块完全解耦合,完全不需要知道其它模块的信息,只需要知道核心组件里的接口就可以调别的组件了。这样做到了在不影响其它模块的情况下,直接替换某组件。而核心组件正是通过MEF,以反射的方式发现组件并动态加载组件。
---
“约定优于配置”的理念
之前看MSDN的WebCast,讲Asp.netMVC2的那集,里面有句话我印象特别深“约定优于配置”。
里面的讲师开了个玩笑“你的项目里要是没有50个配置文件,每个配置文件没有100行,你都不好意思跟别人说你写了个Java项目”。这句话当然夸张了,不过Java里面需要配置文件的地方的确不少。就拿Struts2.0和Asp.netMVC来比较。Struts2.0里你需要些配置文件告诉服务器,哪个是Controller,哪个是View,哪个是Model。但是Asp.netMVC里就很清楚了,里面Controller文件夹里的XxxController文件就是名为Xxx的Controller,View文件夹里、Model文件夹里也是如此。视频的讲师当时说了一句“Controller文件夹里放的当然是Controller了,难道你真的要在View文件夹里创建一个文件名是XxxModel的Controller么?你有这个需求么?”
---
乱七八糟的说了一堆,差不多就这样了。。。
我觉得抛开语法而谈,最主要的还是对底层的控制能力不同。
C# 一开始虽然借鉴 Java,但是目的完全不是为了造一个 better Java,而是造一个 better C++。游戏引擎们偏爱 C# 也是有这一层原因在里面,这一点高赞回答中 @MaxwellGeng 的回答已经足够能说清了。
比如在 C# 里面你能干的:
var x = new int[10]; fixed (int* p = x) { Console.WriteLine(*((long*)p - 1)); // 10 }
上述代码会输出 10,为什么?因为 .NET 中数组的长度存储于数组第一个元素之前的 8 字节内存中。如果你再接着输出 *((long*)p - 2)
,将会直接得到这个对象的 TypeHandle
地址:
Console.WriteLine((long)typeof(int[]).TypeHandle.Value == *((long*)p - 2)); // True
然后拿着这个指针又接着能去访问对象的 MethodTable
。
再有你还可以手动在栈上分配空间:
var x = stackalloc int[2]; // 或者 Span<int> x = stackalloc int[2]; 做安全访存 x[0] = 3; x[1] = 1; Console.WriteLine(x[0] + x[1]); // 4
接着你想绕过 GC 直接手动分配堆内存:
var array = (int*)NativeMemory.Alloc(10, sizeof(int)); array[0] = 1; array[1] = 3; Console.WriteLine(array[0] + array[1]); // 4 NativeMemory.Free(array);
上述调用等价于你在 C 语言中调用的 malloc
,此外还有 AllocAligned
、Realloc
、AllocZeroed
等等,可以直接控制内存对齐。
接下来你想创建一个显式内存布局的结构 Foo
:
var obj = new Foo(); obj.Float = 1; Console.WriteLine(obj.Int); // 1065353216 Console.WriteLine(obj.Bytes[0]); // 0 Console.WriteLine(obj.Bytes[1]); // 0 Console.WriteLine(obj.Bytes[2]); // 128 Console.WriteLine(obj.Bytes[3]); // 63 [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
然后你就成功模拟出了一个 C 的 Union,之所以会有上面的输出,是因为单精度浮点数 1 的二进制表示为 0x00111111100000000000000000000000
,以小端方式存储后占 4 个字节,分别是 0x00000000
、0x00000000
、0x10000000
、0x00111111
。
进一步,你还能直接从内存数据没有任何拷贝开销地构造对象:
var data = stackalloc byte[] { 0, 0, 128, 63 }; var foo = Unsafe.AsRef<Foo>(data); Console.WriteLine(foo.Float); // 1 [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
甚至这样:
var data = 1065353216; var foo = Unsafe.AsRef<Foo>(&data); Console.WriteLine(foo.Float); // 1 [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
从堆内存创建自然也没问题:
var data = new byte[] { 0, 0, 128, 63 }; fixed (void* p = data) { var foo = Unsafe.AsRef<Foo>(p); Console.WriteLine(foo.Float); // 1 } [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
再比如,此时你面前有一个使用 C++ 编写的库,其中有这么一段代码:
#include <cstring> #include <cstdio> extern "C" __declspec(dllexport) char* __cdecl foo(char* (*gen)(int), int count) { return gen(count); }
然后我们编写如下 C# 代码:
[DllImport("./foo.dll", EntryPoint = "foo"), SuppressGCTransition] static extern string Foo(delegate* unmanaged[Cdecl]<int, nint> gen, int count); [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }), SuppressGCTransition] static nint Generate(int count) { var str = Enumerable.Repeat("w", count).Aggregate((a, b) => $"{a}{b}"); return Marshal.StringToHGlobalAnsi(str); } var f = (delegate* unmanaged[Cdecl]<int, nint>)&Generate; var result = Foo(f, 5); Console.WriteLine(result); // wwwww
上面的代码干了什么事情?我们将 C# 的函数指针传到了 C++ 代码中,然后在 C++ 侧调用 C# 函数生成了一个字符串 wwwww
,然后将这个字符串返回给 C# 侧。而就算不用函数指针换成使用委托也没有区别,因为 .NET 中的委托下面就是函数指针。
甚至,如果我们不想让 .NET 导入 foo.dll
,我们想自行决定动态库的生命周期,还可以这么写:
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }), SuppressGCTransition] static nint Generate(int count) { var str = Enumerable.Repeat("w", count).Aggregate((a, b) => $"{a}{b}"); return Marshal.StringToHGlobalAnsi(str); } var f = (delegate* unmanaged[Cdecl]<int, nint>)&Generate; var library = NativeLibrary.Load("./foo.dll"); var foo = (delegate* unmanaged[Cdecl, SuppressGCTransition]<delegate* unmanaged[Cdecl]<int, nint>, int, string>)NativeLibrary.GetExport(library, "foo"); var result = foo(f, 5); Console.WriteLine(result); // wwwww NativeLibrary.Free(library);
上面这些都不是 Windows 专用,在 Linux、macOS 上导入 .so
和 .dylib
都完全不在话下。
再有,我们有一些数据想要进行计算,但是我们想使用 SIMD 进行处理,那只需要这么写:
var vec1 = Vector128.Create(1.1f, 2.2f, 3.3f, 4.4f); var vec2 = Vector128.Create(5.5f, 6.6f, 7.7f, 8.8f); Console.WriteLine(Calc(vec1, vec2)); float Calc(Vector128<float> l, Vector128<float> r) { if (Avx2.IsSupported) { var result = Avx2.Multiply(l, r); float sum = 0; for (var i = 0; i < Vector128<float>.Count; i++) sum += result.GetElement(i); return sum; } else if (Rdm.IsSupported) { var result = Rdm.Multiply(l, r); float sum = 0; for (var i = 0; i < Vector128<float>.Count; i++) sum += result.GetElement(i); return sum; } else { float sum = 0; for (int i = 0; i < Vector128<float>.Count; i++) { sum += l.GetElement(i) * r.GetElement(i); } return sum; } }
可以看看在 X86 平台上生成了什么代码:
vzeroupper vmovupd xmm0, [r8] vmulps xmm0, xmm0, [r8+0x10] vmovaps xmm1, xmm0 vxorps xmm2, xmm2, xmm2 vaddss xmm1, xmm1, xmm2 vmovshdup xmm2, xmm0 vaddss xmm1, xmm2, xmm1 vunpckhps xmm2, xmm0, xmm0 vaddss xmm1, xmm2, xmm1 vshufps xmm0, xmm0, xmm0, 0xff vaddss xmm1, xmm0, xmm1 vmovaps xmm0, xmm1 ret
平台判断的分支会被 JIT 自动消除。但其实除了手动编写 SIMD 代码之外,前两个分支完全可以不写,而只留下:
float Calc(Vector128<float> l, Vector128<float> r) { float sum = 0; for (int i = 0; i < Vector128<float>.Count; i++) { sum += l.GetElement(i) * r.GetElement(i); } return sum; }
因为现阶段当循环边界条件是向量长度时,.NET 会自动为我们做向量化并展开循环。
那么继续,我们还有ref
、in
、out
来做引用传递。
假设我们有一个很大的 struct
,我们为了避免传递时发生拷贝,可以直接用 in
来做只读引用传递:
void Test(in Foo v) { } struct Foo { public long A, B, C, D, E, F, G, H, I, J, K, L, M, N; }
而对于小的 struct
,.NET 有专门的优化帮我们彻底消除掉内存分配,完全将 struct
放在寄存器中,例如如下代码:
double Test(int x1, int y1, int x2, int y2) { var p1 = new Point(x1, y1); var p2 = new Point(x2, y2); return GetDistance(p1, p2); } [MethodImpl(MethodImplOptions.AggressiveInlining)] double GetDistance(Point a, Point b) { return Math.Sqrt((a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y)); } struct Point { public Point(int x, int y) { X = x; Y = y; } public int X { get; set; } public int Y { get; set; } }
上述代码 GetDistance
考虑是个热点路径,因此我加 MethodImplOptions.AggressiveInlining
来指导 JIT 有保证地内联此函数,最后为 Test
生成了如下的代码:
vzeroupper sub ecx, r8d mov eax, ecx imul eax, ecx sub edx, r9d mov ecx, edx imul edx, ecx add eax, edx vxorps xmm0, xmm0, xmm0 vcvtsi2sd xmm0, xmm0, eax vsqrtsd xmm0, xmm0, xmm0 ret
全程没有一句指令访存,非常的高效。
我们还可以借用 ref
的引用语义来做原地更新:
var vec = new Vector(10); vec[2] = 5; Console.WriteLine(vec[2]); // 5 ref var x = ref vec[3]; x = 7; Console.WriteLine(vec[3]); // 7 class Vector { private int[] _array; public Vector(int count) => _array = new int[count]; public ref int this[int index] => ref _array[index]; }
甚至还能搭配指针和手动分配内存来使用:
var vec = new Vector(10); vec[2] = 5; Console.WriteLine(vec[2]); // 5 ref var x = ref vec[3]; x = 7; Console.WriteLine(vec[3]); // 7 unsafe class Vector { private int* _memory; public Vector(uint count) => _memory = (int*)NativeMemory.Alloc(count, sizeof(int)); public ref int this[int index] => ref _memory[index]; ~Vector() => NativeMemory.Free(_memory); }
C# 的泛型不像 Java 采用擦除,而是真真正正会对所有的类型参数特化代码(尽管对于引用类型会共享实现采用运行时分发),这也就意味着能最大程度确保性能,并且对应的类型拥有根据类型参数大小不同而特化的内存布局。还是上面那个 Point
的例子,我们将下面的数据 int
换成泛型参数 T
,并做值类型数字的泛型约束:
double Test1(double x1, double y1, double x2, double y2) { var p1 = new Point<double>(x1, y1); var p2 = new Point<double>(x2, y2); var result = GetDistanceSquare(p1, p2); return Math.Sqrt(result); } double Test2(int x1, int y1, int x2, int y2) { var p1 = new Point<int>(x1, y1); var p2 = new Point<int>(x2, y2); var result = GetDistanceSquare(p1, p2); return Math.Sqrt(result); } [MethodImpl(MethodImplOptions.AggressiveInlining)] T GetDistanceSquare<T>(Point<T> a, Point<T> b) where T : struct, IBinaryNumber<T> { return (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y); } struct Point<T> where T : struct, IBinaryNumber<T> { public Point(T x, T y) { X = x; Y = y; } public T X { get; set; } public T Y { get; set; } }
无论是 Test1
还是 Test2
,生成的代码都非常优秀,不仅不存在任何的装箱拆箱,甚至没有任何的访存操作:
; Test1 vzeroupper vsubsd xmm0, xmm0, xmm2 vmovaps xmm2, xmm0 vmulsd xmm0, xmm0, xmm2 vsubsd xmm1, xmm1, xmm3 vmovaps xmm2, xmm1 vmulsd xmm1, xmm1, xmm2 vaddsd xmm0, xmm1, xmm0 vsqrtsd xmm0, xmm0, xmm0 ret ; Test2 vzeroupper sub ecx, r8d mov eax, ecx imul eax, ecx sub edx, r9d mov ecx, edx imul edx, ecx add eax, edx vxorps xmm0, xmm0, xmm0 vcvtsi2sd xmm0, xmm0, eax vsqrtsd xmm0, xmm0, xmm0 ret
接着讲,我们有时候为了高性能想要临时暂停 GC 的回收,只需要简单的一句:
GC.TryStartNoGCRegion(1024 * 1024 * 128);
就能告诉 GC 如果还能分配 128mb 内存那就不要做回收了,然后一段时间内以后的代码我们尽管在这个预算内分配内存,任何 GC 都不会发生。甚至还能阻止在内存不够分配的情况下进行阻塞式 Full GC:
GC.TryStartNoGCRegion(1024 * 1024 * 128, true);
代码执行完了,最后的时候调用一句:
GC.EndNoGCRegion();
即可恢复 GC 行为。
除此之外,我们还能在运行时指定 GC 的模式来最大化性能:
GCSettings.LatencyMode = GCLatencyMode.Batch; GCSettings.LatencyMode = GCLatencyMode.Interactive; GCSettings.LatencyMode = GCLatencyMode.LowLatency; GCSettings.LatencyMode = GCLatencyMode.NoGCRegion; GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
更进一步,我们甚至可以直接将堆内存中的代码执行,在 .NET 上自己造一个 JIT,直接从内存创建一块可执行的区域然后往里面塞一段代码用来将两个32位整数相加:
var kernel32 = NativeLibrary.Load("kernel32.dll"); var virtualProtectEx = (delegate* unmanaged[Cdecl, SuppressGCTransition]<nint, void*, nint, int, out int, bool>)NativeLibrary.GetExport(kernel32, "VirtualProtectEx"); var processHandle = Process.GetCurrentProcess().Handle; Memory<byte> code = new byte[] { 0x8d, 0x04, 0x11, // lea rax, [rcx+rdx] 0xc3 // ret }; using (var handle = code.Pin()) { virtualProtectEx(processHandle, handle.Pointer, code.Length, 0x40, out _); var f = (delegate*<int, int, int>)handle.Pointer; Console.WriteLine(f(2, 3)); // 5 } virtualProtectEx = null; NativeLibrary.Free(kernel32);
除此之外,C# 还有更多数不清的底层写法来和操作系统交互,甚至利用 C# 的编译器取消链接到自己的标准库,直接用从 0 开始造基础类型然后通过 NativeAOT 编译出完全无 GC、能够在裸机硬件上执行引导系统的 EFI 固件都是没有问题的,参考 https://github.com/MichalStrehovsky/zerosharp
另外还有 ILGPU 让你把 C# 代码直接跑在 GPU 上面,以及跑在嵌入式设备上直接操作 I2C、PWM、GPIO 等等,就不再举例子了。
而 C# 已经进了 roadmap 的后续更新内容:允许声明引用字段、添加表达固定长度内存的类型、允许传数组时消除数组分配、允许在栈上分配任何对象等等,无一不是在改进这些底层性能设施。
以上就是我认为的 C# 和 Java 最大的不同。
在 C# 中当你不需要上面这些的东西时,它们仿佛从来都不存在,允许动态类型、不断吸收各种函数式特性、还有各种语法糖加持,简洁度和灵活度甚至不输 Python,非常愉快和简单地就能编写各种代码;而一旦你需要,你可以拥有从上层到底层的几乎完全的控制能力,而这些能力将能让你有需要时无需思考各种奇怪的 workaround 就能直接榨干机器,达到 C、C++ 的性能,甚至因为有运行时 PGO 而超出 C、C++ 的性能。
微软的c#设计者更注重一线开发人员的感受,为方便开发提高效率,他们愿意大费周章改善语言本身各方特性,不断加入语法糖,从泛型,nullable,隐式类型到lamada再到dynamic,awaitasyc等等都可看到其一直在围绕代码整洁,减少bug等实际的开发过程中问题来进行的改进,同时越来越智能的IDE也说明了这点。
而java设计者则不同,他们的关注点在于java应用系统本身,更好的降低耦合,保持OOP是其始终坚持的。同时也应该不难发现,社区对java应用架构师提出的各类尖锐问题反馈总是较为及时,而对java应用开发者则相对冷淡些。正因此,使用java开发的大型应用系统相比同等代码量c#开发出的系统至少在系统结构上要比c#更加美观,易维护,代码变腐烂的速度也更慢。加上java及linux均为开源产品,许多大型公司又在基于成本的考虑上最终还是选择使用java进行应用系统开发。
最后拿泛型举例看看c#和java对待同一问题时各方的区别:c#在2.0中为推出泛型特性是对预编译,编译及运行时做了根本性改变的,而java设计者则显然不够"诚意",他们在预编译期直接将泛型"处理"掉了,你在运行时根本看不到泛型这一特性。我臆测java设计者可能认为即使没有泛型,应用系统设计师通过优秀的程序设计是可以解决list类型不一致问题的,这种通过添加语法规则规避问题的做法实际是懒惰思维,添加了泛型支持也只是向社区的呼声做了一次妥协。
面对同一问题采用不同处理方法的结果是,c#基于泛型后续衍生出了lamada,linq等一系列更能提高开发效率的语法利器,而java因有了泛型的强制约束使得java开源中间件变得更加稳定,同时可以看到后续的许多开源中间件产品体量也变得更大,功能也开始更加大胆。这也算是通过泛型确保类型一致带来的好处之一吧。
所以,虽语法非常相似,但关注方向并不相同的两种语言至少目前来说区分优劣为时尚早了些,但就趋势来说,c#的发展势头要更为强劲,不去关注两种语言本身,就平台而言,微软对c#的掌控力要绝对优于java社区对java平台的掌控力,c#设计者在改造c#语言时不需要做出太多妥协,他们考虑最多的只是5.0如何向4.5兼容,4.5如何向4.0兼容,而java在发展的过程中每一步跨越需要考虑的则更多。同时,最要命的,java的一切设计都是牢牢基于面向对象的,然而从实际的开发经验来看,现实世界中的许多问题并不能单纯依靠面向对象来解决,牵强使用OOP给实际系统开发已经带来不少的负面影响,例如,红苹果到底是苹果的子类还是红色是苹果的一个属性?有人会觉得这要联系上下文才能知道究竟应该如何定义这颗苹果,然而联系上下文的过程本身就是一个寻找妥协的过程,可以符合当前系统设计要求,但,不代表系统需求不会改变,而在敏捷开发中,我们首先要树立的思想便是"需求是迟早会变的",这种矛盾在面向对象的开发过程中会长期相伴。回头再看c#,虽然其出身也是彻头彻尾的面向对象,然而在其后续的演进过程中似乎并未把OOP放在眼里,到c# 3时干脆加入了具有函数式编程特性的linq,而当其与原先就支持的委托特性结合时,其强大之处就可见一斑了,原先需要使用OOP思想编写的大量"难看"代码linq居然只要几行就能搞定,且更加符合自然语言表达。而java支持lamda似乎是linq推出整整6年(也许还不止)后的最近了。
回到问题本身回答题主,综上,C#和JAVA的区别有很多,而最大的区别在于他们的语言(平台)设计者的关注点并不相同。
手机打字,部分语句不通顺望见谅。
原来到在知乎这样严谨的地方也会有如此多不负责任的回答来误导大家。
Java 是大家共有的,.NET 是微软独有的
java是oracle的,不属于你和任何人的。android因为使用了java语言而被oracle起诉要求赔偿26亿美元。
oracle | 雷锋网
Java跨不同种平台,windows,linux,mac,other unixlike,other
c#(.net)跨不同版本windows的平台 xp,vista,windows7,win8,CE.....
.net一样可以跨平台运行,除了windows Linux, FreeBSD, Unix, Mac OS X和Solaris Android之外,甚至还比java多了一个IOS平台。
参考资料:
mono_百度百科
MonoTouch_百度百科
Mono for Android首页、文档和下载
至于java号称的一次编写各平台运行,有过开发经验的人都知道这只是个笑话而已。连html5这样专门为跨平台出生的东西现在都有诸多兼容性问题。
Java的跨平台就是一句谎言 Java的跨平台就是一句谎言。
.net跨平台也是一句谎言 .net 跨平台也是一句谎言
跨平台就是一种谎言 跨平台就是一种谎言
java是专业相机,.net是傻瓜相机
这种说法我觉得我觉得很搞,而且没有任何逻辑支撑。任何java可以做的事情,c#都可以做,并且可以代码更简洁。
我认为java语言本身是一个落后的语言,这里我只说语言本身不扯其他的。
我知道会有很多java fans会反驳我,这里我不会跟贴讨论java语言的优缺点。
事理越辩越明,那么就来详细说说为什么我不喜欢Java*语言*
如果要我来形容,我觉得c#是一个朝气蓬勃,身具各家所长的年轻人。java是一个老态龙钟,吃老本的老年人。
---------------------------------
update 2013.12.10
这个答案大概回答于2012年,当时只有4,5个回答。
我觉得当时的几个回答具有误导性,所以把这几条回答的结论提取出来并写出我的解读。
一年后的今天本问题已经有很多优秀的答案,而我提取结论的回答已经被淹没,所以今天这条被顶上来回答看起来有些莫名其妙,导致评论里出现一些误解和不和谐的言论,我不想引起争端,直接删除了这些评论,并且关闭评论功能,望理解,谢谢!
先下结论:电影想把Freddie塑造成一个有人性的神,却忘了真正的Freddie只是一个有神性的人
如果作为一部粉丝向的情怀片,《波》已经达到了满分,哪怕不谈对细节出色的把控,光是最后二十分钟的神级还原已经足够让所有的情怀在we are the champions中泪流满面
感受一下当时的直播:
Live Aid https://www.zhihu.com/video/1092941240030597120
但是作为一部传记片,《波》还是太流程化了,才华横溢的主角惊艳出场,遇到小人,遭遇挫折,众叛亲离,踢开小人,亲友重聚,完美收场。作为人物小传也算及格,但是对于Freddie这样的传奇人物的剖析还是不够大胆,想要表现其人性的一面,又不敢去探索Freddie其实也有自私功利的角落,想要表现其亦男亦女的魅力,却又只是浮于外表没有触碰到灵魂,以至于片子自始至终有种畏手畏脚的憋屈感。
不过不管受众是谁,《波西米亚狂想曲》至少是一部及格线以上的作品,再加上Queen的音乐加成,哪怕不至于血脉喷张,但让观众在电影院点点头抖抖腿还是绰绰有余了
看完电影之后,再看到波西米亚狂想曲的歌词,或许会有一些不一样的体会
Is this the real life
Is this just fantasy
Caught in a landslide. No escape from reality
Open your eyes.Look up to the skies and see
I'm just a poor boy, I need no sympathy
Because I'm easy come, easy go,A little high, little low,
Anyway the wind blows, doesn't really matter to me
freddie的生命像一场华丽的错觉,但他所留下来的,is not fantasy
这是我看到的最准确的总结。
总的来说,就是中国的高考相对公平,所以性价比极高,所以其他活动都可以适当让步。