程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#特性雜談

C#特性雜談

編輯:C#入門知識

文中充滿了各種C#與其他語言的對比及吐槽, 希望介意者勿觀… 當然, 鑒於太亂, 我懷疑有沒有人能看完.

  • 學習C#
  • Hello World
  • 變量與表達式
    • 動態類型
    • 值類型和引用類型
    • checked支持的受限強制轉換
  • 流程控制
  • 數組
  • 函數
    • 可選參數
    • 命名參數
    • 委托(delegate)
    • 匿名函數(Lambda)
  • 異常
  • 面向對象部分
      • 構造函數初始化器
      • 訪問器
      • 可訪問性傳遞原則
      • 匿名類
      • 擴展方法
    • 接口
      • 顯示實現接口成員
    • 可刪除對象
  • 結構
  • 泛型
    • 約束類型
    • 可空變量
  • 事件
  • 最後

學習C#

C#也的確不是當年那種微軟, Windows獨占的語言了, mono項目已經將C#移植到了Mac和Linux上, 甚至還包括iOS和Android. 也就是說, 假如你願意的話, 你可以使用C#通吃所有平台, 當然, 前提是你能接受巨貴的授權費用. 作為什麼事情都喜歡自己搞一套的微軟,(因為他都是壟斷的) 在C#這件事情上一開始就很開放(因為有JAVA在前), 總算是做對了一件事情.

Hello World

Mono在創建一個名為test的console的工程後, 給了我一個Hello World的代碼,

using System;

namespace test
{
  class MainClass
  {
    public static void Main (string[] args)
    {
      Console.WriteLine ("Hello World!");
    }
  }
}

第一眼看去, 就知道C#中了JAVA的毒了, 用所謂完全面向對象的方式, 強迫你寫一堆臃腫而無用的代碼… 我至今也沒有明白為什麼Main函數最後會變成Main類, 也沒有明白這有什麼好處… 想起最近看的一篇文章Why Java Sucks and C# Rocks, 說”自從C# 1.0誕生之日起,就只出現Java借鑒C#特性的情況”, 以此來駁斥JAVA一派對C#抄襲的指責. 其實, 光是從Hello World都能看出來C#對JAVA的模仿, 他的言論也的確是避重就輕了, 因為, 連他也無法否認C#誕生前及誕生過程中發生的事情…

值得一提的是, C#對傳統的printf進行了改進, {0}形式的占位符不需要表明類型, 只按照數量和位置匹配.

變量與表達式

動態類型

大家都知道類似C/C++, JAVA, C#這種對效率還稍微有些追求的語言都是靜態類型語言, 並且靠編譯期靜態類型檢查來排查錯誤, 而從C++以後, 各語言都以更加’真正的’強類型自豪. 而類似auto,var等自動類型推導的變量只不過是語法糖而已, 所以當我看到C#的確提供了動態類型dynamic, 我還是著實吃了一驚. 這也體現了C#的設計者們比C++, JAVA更加激進的一面. 很多年前, BS就說過(其實Mats也說過類似的), 語言不是一堆特性的堆積, 也不是說堆積的越多就越好. 因為, 很多時候JAVA, C#的擁護者們炫耀著JAVA, C#有著什麼樣的新特性的時候, 其實並不太感冒. 但是, 這一個, 夠讓人震撼的. 一個靜態語言裡面有動態類型會是什麼效果? 不禁想讓人嘗試一番.

public static dynamic Add(dynamic var1, dynamic var2)
{
  return var1 + var2;
}

類似上面的Add功能, 本來必須使用模版才能實現, var使用時, 因為是類型推導, 所以必須在初始化時才能使用, 不能用於函數參數, 而真正的動態類型dynamic就可以. 最近使用Python, Ruby比較多, 突然感覺C#有種Python, Ruby上身的感覺.

值類型和引用類型

當我看到Why Java Sucks and C# Rocks(2):基礎類型與面向對象一文時, 我還以為C#真的已經是所謂的”完全面向對象”了. 所以當我看到C#入門經典書中寫到變量的類型還是分為引用類型和值類型時, 我相當意外.(我先看的那一系列文章, 再看的C#入門經典) 等我看到書中寫到封箱和拆箱的時候, 就更加驚訝了… 都是一個對象, 為啥還要封箱和拆箱呢? 當然, 鑒於Objective-C連自動的封箱和拆箱都還沒有, 我也不能說這就有多麼落後. 
不過, 我用C++的時候就完全沒聽說過封箱和拆箱的概念, 容器的設計完全可以容納基礎數值, 為啥到了Objective-C, JAVA和C#裡反而不行了呢? 因為C++沒有統一的基類? 容器設計的時候就壓根不是光考慮存儲啥Object對象的. 這個倒是讓我想起一句話, 你以為你解決了一個問題, 因為你比以前更加優美了, 但是同時帶來了另外一個問題, 後來, 你們比較的是後一個問題誰更優美的解決了. 而這個問題本來並不存在…
另外, 當你其實還分值類型和引用類型的時候, 你就已經輸給Python, Ruby了, 何必還討論誰的箱子更好看呢… 要把這個問題上升到理念層次, 我更加就沒法認同了.

checked支持的受限強制轉換

增加了checked, unchecked(默認)關鍵字來應付類型轉換時的溢出問題. 比如下面的代碼:

short source = 257;
byte dest = (byte)source;

上面的代碼在轉換時會發生溢出, 這往往不是我們要的結果, 也往往因此出現莫名而難以調試的bug. 而類似下面的代碼會在運行時會拋出System.OverflowException: Number overflow.異常. 這個方案很值得欣賞. 簡單有效.

流程控制

數組

函數

可選參數

實際上就等於C++裡面的參數默認值, 在有默認值時, 在函數最後的參數為可選. 看書中說C#是在C#4後才支持, 為啥呢?

命名參數

命名參數在動態語言裡面是很常見的, 並且是個容易理解又很使用的功能. 但是C++並沒有支持, 看BS在C++語言的設計與演化中講到其實當時有過討論, 只是因為在C++中有所謂的接口與實現分離, 而看以前的代碼, 很多接口(頭文件)使用的參數名和實現中用的參數名並不一樣, 所以這個有用的特性並沒有加入C++. 這也是為什麼我前面說接口與實現分離實在是太不DRY的一個原因.
當時BS給的例子是用Win32 API創建windows的代碼(因為書在同事那裡, 記憶不准確請指出), 因為需要的參數實在是太多了. 非常的不方便, 見MSDN:

HWND WINAPI CreateWindow(
    _In_opt_  LPCTSTR lpClassName,
    _In_opt_  LPCTSTR lpWindowName,
    _In_      DWORD dwStyle,
    _In_      int x,
    _In_      int y,
    _In_      int nWidth,
    _In_      int nHeight,
    _In_opt_  HWND hWndParent,
    _In_opt_  HMENU hMenu,
    _In_opt_  HINSTANCE hInstance,
    _In_opt_  LPVOID lpParam
    );

事實上, 在Win32 API裡面, 你要完整的創建一個窗口, 還有類似注冊窗口類等巨多參數的接口, 而其實在這個API裡面, 每次調用時真正需要使用的又並不是所有的參數, 不用說有多不方便了. 可選參數(參數默認值), 只能在參數列表的最後使用, 讓這種簡化有的時候變成了一個排序游戲, 到底哪個參數才是最不常用的呢?
BS給了在C++裡面我們的一種解決方案, 這種方案也是我們在實際中使用的方案, 那就是用struct, 當struct成員變量都有默認值的時候, 我們就只需要給我們真正需要的那個變量賦值即可. 具體的情況就不多說了, 書上都有, 但是在有命名參數後這些都是浮雲. 你只需要給你的確需要的參數賦值即可, 也不需要額外的創建類或結構. 比如上例, 有了命名參數後, 我假如只對窗口的寬度感興趣, 那麼如下調用即可:

public static int CreateWindow (
  int lpClassName = 0,
  int lpWindowName = 0,
  int dwStyle = 0,
  int x = 0,
  int y = 0,
  int nWidth = 0,
  int nHeight = 0,
  int hWndParent = 0,
  int hMenu = 0,
  int hInstance = 0,
  int lpParam = 0
)
{
  return 0;
}

    public static void Main (string[] args)
{
  CreateWindow(nWidth: 640, nHeight: 960);
}

還有比這更方便的事情嗎? 順面吐槽一句, Objective-C裡面函數調用的方式簡直就是為命名參數准備的, 當然竟然完全不支持命名參數, 甚至不支持參數默認值, 崩潰啊…

委托(delegate)

這算是接觸到的第一個較新的概念, 多寫一點.
delegate是Objective-C裡面用的非常多的概念, 有很方便的一面, 但是是在類這個層次上的概念.
C#的委托更加想是Objective-C的SEL/@selector和C++ 11的function, 也就是為了方便函數調用(特別是回調函數)和構建高階函數而存在的. 這個也是函數不是第一類值(first class)的語言裡面需要解決的問題. 函數指針有人說很方便, 但是那個語法實在太逆天了. 當然, 因為這個原因, 其實C#的委托也無法實現直接對<, >, +, -等操作符的控制, 而是需要用類似C++的方法提供輔助函數的方法來完成.

C#的委托:

using System;

namespace test
{
  class MainClass
  {
    delegate int Actor (int leftParam, int rightParam);

    static int Call (Actor fun, int leftParam, int rightParam)
    {
      return fun(leftParam, rightParam);
    }

    static int Multiply (int leftParam, int rightParam)
    {
      return leftParam * rightParam;
    }

    static int Divide (int leftParam, int rightParam)
    {
      return leftParam / rightParam;
    }
    public static void Main (string[] args)
    {
      Console.WriteLine ("{0}", Call(new Actor(Multiply), 10, 10));
      Console.WriteLine ("{0}", Call(new Actor(Divide), 10, 10));
      Console.ReadKey();
    }
  }
}

匿名函數(Lambda)

有匿名函數的語言才算是現代語言啊… 我說這句話, C++, JAVA, Objective-C, C#無一中槍, 不管是加入的早晚(其實都是較晚), 上述語言都已經有了使用匿名函數的辦法. 對於Python, Ruby來說, 匿名函數就更不是什麼新鮮事物了. 比較有意思的是, 作為靜態語言的新事物, 上述語言都獨立的發展了一套自己的Lambda語法, 而且各有特色, 並且最終的目的似乎都是讓你搞不明白. C#的Lambda使用了=>來標記. 因為可以使用類型推導, 所以語法的簡潔性上可以做到極致.
參考上面委托的例子, 假如Multiply和Divide我們只是使用一次的話, 還按上面的形式定義就太麻煩了, 匿名函數可以簡化代碼.

class MainClass
{
  delegate int Actor (int leftParam, int rightParam);

  static int Call (Actor fun, int leftParam, int rightParam)
  {
    return fun(leftParam, rightParam);
  }

  public static void Main (string[] args)
  {
    Console.WriteLine ("{0}", Call((leftParam, rightParam) => {
          return leftParam * rightParam;
          },
          10, 10));
    Console.WriteLine ("{0}", Call((leftParam, rightParam) => {
          return leftParam / rightParam;
          },
          10, 10));
    Console.ReadKey();
  }
}

可以看到代碼顯著的簡化, 當然, 其實這裡的例子還是太過簡單和生造了, 匿名函數最大的應用在於利用閉包特性來作為回調函數. 此時簡化的往往不僅僅是一個函數, 甚至是一套完整的類. 而且, 在多個類似回調同時在一個類中使用的時候, 每個匿名函數各自獨立, 不會出現需要在類的回調函數中用switch/if-else區分的丑陋代碼. 這個回憶回憶以前你用過的任何GUI系統的Button回調, 大概就能理解. 而最佳的例子, 我也常常提起我很感歎的是, 在最近的Objective-C中加入的匿名函數(在Objective-C中被稱為Block)對Objective-C接口的巨大影響, 幾乎是整體性的對原有delegate的替換. 這個替換過程不僅僅發生在社區, 連Apple官方也是如此.

異常

傳統的try, catch, finally模式.

面向對象部分

主要有價值的特性都在這一部分. 我覺得最大的改進就在於C#和JAVA都沒有使用C++(還有Objective-C)裡面看似優美的接口與實現分離的策略. 很多時候我們都在說DRY(Don’t Repeat Yourself)是編程中排在第一的原則, 但是接口與實現分離, 即一個頭文件用於聲明, 一個實現文件用於實現的方式是徹頭徹尾的Repeat. 這種方式就我了解是來自於C語言. C++和Objective-C都在一定程度上有向C語言兼容的負擔, 於是都這樣做了. 稍微追求點人性化的語言其實都不是類似C/C++和Objective-C那種方式. 這個序列可以從JAVA, C#一直寫到lua, python, ruby, lisp.
這裡有個值得探討的話題, 因為我的確對編譯原理什麼的不是太了解, 不過大概知道, 實際的我們稱的編譯過程包括編譯(生成.o文件)及鏈接(生成真正的可執行文件)兩個過程, 而接口與實現分離的好處在於, 編譯期可以不要求找到實現, 方便單獨對每個文件的編譯和將實現放在別的文件裡面, 直到鏈接期才真正的匹配. 這也許是早期類C語言(需要直接最後生成機器碼)這麼設計的根源. 但是, 我還是得說, 那種設計並不好, 並且其實是有辦法避免的. 只是可能需要離C遠一點, 所以C++和Objective-C都沒有用. 更有意思的是, 盡管似乎早就已經有export關鍵字了, C++還是只能將模版的聲明和實現都放在頭文件裡面, 而我當年, 甚至還覺得模版這麼做實在是太不規范了. 當然, 還有inline, 這也是個必須放在頭文件裡面的家伙.

構造函數初始化器

支持類似C++初始化列表的構造函數初始化器. 語法也類似.
在新的C#中還支持直接類似C++ 11的統一初始化格式的對象初始化器. 語法如下:

namespace test
{
  public class Point
  {
    public int x {get; set;}
    public int y {get; set;}
  }

  class MainClass
  {
    public static void Main (string[] args)
    {
      Point p = new Point { x = 1, y = 2 };
      Console.WriteLine("Point: x={0}, y={1}", p.x, p.y);
    }
  }
}

其中Point p就是通過上述對象初始化器完成的初始化. 這種方法沒有提供一個帶完善參數的構造函數使用起來方便, 但是在沒有提供類似構造函數的時候. 可以不需要一行一行直接使用定義後的變量來初始化了. 另外, 對於集合來說, 也有類似的語法.

訪問器

通過get, set關鍵字來定義屬性的訪問器, 並且通過忽略其中一個來實現只讀和只寫. 並且提供了一種自動生成屬性的功能, 代碼類似public int MyIntProp { get; set;}, 在一行內定義一個屬性, 並且不用再定義一個變量, 這個變量由編譯器自動生成, 我們不知道它叫什麼, 但是能通過訪問器提供的方法來訪問.

可訪問性傳遞原則

可訪問性只能越來越嚴格, 不能越來越放松. 比如internal類可以繼承public類, 反過來不行. 訪問器的getset單獨設置的訪問限制也類似, 只能比統一外部聲明的要更加嚴格.

匿名類

大家都知道匿名函數好用, 匿名類呢? 有了總比沒有好吧.
配合var使用, 語法如下:

var p = new { x = 1, y = 2};
Console.WriteLine("Point: x={0}, y={1}", p.x, p.y);

注意, 我其實並沒有定義一個Point類. 包含x,y成員變量的類由編譯器自動生成.

擴展方法

在Python, Ruby裡面都能很方便的給已存在的類添加方法, 實現打猴子補丁的功能, 類也被稱為開放類. 當時靜態語言一般不行, C#通過擴展方法實現類似的功能, 只不過語法非常之不優美, 同為靜態語言, 建議anders去學習一下Objective-C裡面的類別(category), 這種使用在Objective-C中非常的普遍, 因為的確非常的方便.
首先看C#的語法:

public static class ExtensionString
{
  public static List<char> GetArray (
      this string str)
  {
    List<char> result = new List<char>();
    foreach (char c in str) {
      result.Add(c);
    }

    return result;
  }
}

class MainClass
{
  public static void Main (string[] args)
  {
    string s = "abcdefg";
    List<char> chars = s.GetArray ();

    foreach (char c in chars) {
      Console.Write(c);
    }
  }
}

上例中, 我給標准的string類增加了一個GetArray的接口, 返回一個List<char>類型的對象. 語法大概的描述如下:

不知道大家怎麼看, 我是覺得有些不太直觀和自然.

接口

支持類似JAVA的接口. 關鍵字interface. 訪問限定永遠是public(不然還做接口干啥), 無實現代碼, 不定義成員變量. 但是可以定義屬性. 並且語法類似自動屬性.
繼承時沒有使用extendsimplement等關鍵字, 要求實現繼承的基類放在第一個位置(沒有則可以忽略), 接口都放在後面即可. 類似下面的語法:

public class MyClass : MyBase, IMyInterface1, IMyInterface2
{
  // class members
}

也就是說, C#的繼承體系基本上和JAVA一樣, 只允許面向接口(規格)的多重繼承, 不允許面向實現的多重繼承. 這樣好不好就見仁見智了(可參考多重繼承不好的觀點是錯誤的一文), 不過基本可以肯定的是, 的確要比C++不受限制的多重繼承要難以濫用.

顯示實現接口成員

又一個新東西, 在實現類明確的制定一個函數是實現接口的某個函數式, 只能通過接口的多態性來調用該函數, 不允許使用類本身的對象來調用. 這相當於強制接口調用.

可刪除對象

雖然C#有垃圾回收機制, 但是大家都知道, 自動的垃圾回收機制使得程序員對內存及資源的掌握變弱了, C#使用可刪除對象來解決這個問題, 提供了一個IDisposable接口, 限定必須實現Dispose()接口(相當於手動調用的析構函數, 而不是垃圾回收機制自動調用的析構函數), 很有意思, 這個時候的用法就很類似C++了.

using System;

namespace test
{
  class MainClass
  {
    class NeedDispose : IDisposable {

      public NeedDispose() {
        Console.WriteLine("Constructing");
      }

      public void Dispose ()
      {
        Console.WriteLine("I'm Disposed");
      }
    }
    public static void Main (string[] args)
    {
      NeedDispose dispose = new NeedDispose();

      dispose.Dispose();
    }
  }
}

到這兒不算完, 都要手動調用的話, 那還不如C++那樣出了對象存活范圍就自動析構的對象, 於是多了個using的用法, 基本實現了C++使用對象的方式對資源的管理.

// 語法一:
using (NeedDispose dispose = new NeedDispose())
{
}

// 語法二:
NeedDispose dispose = new NeedDispose();
using (dispose) 
{
}

不過有點比較奇怪的是, 兩種語法形式上有區別, 但是本質上竟然一樣, 語法一的定義在出了using的scope以後竟然還有效, 也就是說, 上面那兩種語句同時在一個scope中出現時, 會出現dispose的重定義沖突, 這個設計很奇怪.

結構

泛型

C#在2.0後才加入了泛型, 作為一門新語言, 不知道這是為啥. 這也是為啥前面提到不知道從哪冒出來的封箱拆箱問題的原因?
泛型容器的好處就是它是強類型的, 對於強類型語言來說, 不提供強類型的容器, 那還叫強類型嗎?(行吧, Objective-C躺著中槍了, 它至今還沒有泛型)

約束類型

這個類似C++中的曾經想要(但是沒有)加入C++ 11的特性concept的更通用版本. 相當於給泛型類型一個約束限定, 只允許符合約束條件的模版類型. 這個特性的加入也體現了委員會和公司決定的語言之間的區別. 委員會保守, 公司激進, 而個人? 隨意! 參考Python3…

interface IMyInterface
{
  void DoSomeThing();
}

class MyClass : IMyInterface
{
  public void DoSomeThing() {

  }
}

class MyGenericClass<T> where T : IMyInterface
{

}

class MainClass
{
  public static void Main (string[] args)
  {
    // MyGenericClass<int> x = new MyGenericClass<int>();  // compile error
    MyGenericClass<IMyInterface> y = new MyGenericClass<IMyInterface>();
    MyGenericClass<MyClass> z = new MyGenericClass<MyClass>();
  }
}

語法上見上面的代碼, 其中被注釋掉的那一行會出現編譯錯誤, 因為模版類型T被約束了. 只有IMyInterface及其子類才可以使用MyGenericClass<T>模板類.

可空變量

即可以等於null的變. 用類似int?的形式來定義一個可空類型, 實際是System.Nullable<int>類型的縮寫. 用??操作符來為可空類型提供默認值.

事件

C#的事件本質上就是一種Gof的observer設計模式, 雖然語法上沒有用subscribe這些傳統概念. 並且因為C#委托的存在, 事件寫起來還算是比較方便. 獨創的用操作符+=用於表示事件的訂閱, 絕對是屬於操作符能自定義後被濫用的絕佳例子.

最後

結論是, 假如不知道C#的那些高級特性, 那麼把C#當作C++來用幾乎沒有任何問題, 而那些高級特性, 完全可以逐步的嘗試. 而且JAVA和C#等語言的的確是進步了, 進步的方式就是把C++好的習慣用法, 編程規范和最佳實踐, 直接變成語言特性(除了那所謂的完全面向對象). 一些改動雖然看起來很小, 甚至是語法糖, 但是的確是強制的(或者語言層面鼓勵)寫更好的代碼.
當然, 加入新特性時, 要比那個該死的委員會(雖然BS強調過C++不是委員會設計)效率要高太多了. 這也是我較為欣賞的一點, 也許有人可以說穩定正是C++的好處, 但是在快速變化的互聯網時代, ‘不進則退’啊… 這個話也就只能安慰安慰C++程序員罷了, 市場的丟失是實實在在的. 作為一個從C++入門的程序員, 對此我常常倍感痛心.
本文純粹是C#入門經典關於C#語言部分的閱讀記錄, 其中牽扯到的各種語言大部分都是憑借我的記憶, 語言過多了總難免記憶混亂, 其中如有錯誤的之處希望大家能不吝賜教.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved