C# 是 TypeScript 的最佳替代品?

C# 是 TypeScript 的最佳替代品?

TypeScript 非常優秀。它完美地結合了強型別和快速開發,因此非常好用,我在許多情況下都會預設選擇這個函式庫。但是,世上沒有完美的語言,有些情況下 TypeScript 並不是最合適的工具:

最後更新 2021/12/27 下午8:53
CSDN
預計閱讀 13 分鐘
分類
.NET
標籤
.NET C# TypeScript

作者 | Nate Hill

譯者 | 彎月

出品 | CSDN(ID:CSDNnews)

TypeScript 非常優秀。它完美地結合了強型別和快速開發,因此非常好用,我在許多情況下都會預設選擇這個庫。但是,世上沒有完美的語言,有些情況下 TypeScript 並不是最合適的工具:

  1. 效能至關重要(例如即時通訊、影片遊戲)
  2. 需要與原生程式碼(如 C/C++或 Rust)互動
  3. 需要更嚴格的型別系統(例如金融系統)

對於這些情況,TypeScript 開發人員最好還是選用其他語言。C#、Go 和 Java 都是非常好的選擇。它們的速度遠超 TypeScript,每種語言都有自己的長處。C#能與 TypeScript 配合得很好,我來解釋一下為什麼。

圖源:CSDN 付費下載自東方 IC

1. TypeScript 就是新增了 C# 的 JavaScript

C#能與 TypeScript 配合得很好,因為它們看上去就像是同一種語言。兩者都是由 Anders Hejlsberg 設計的,而且從許多方面來看,TypeScript 就是新增了 C#的 JavaScript。它們的特性和語法都很相似,因此在同一個專案中結合使用二者非常容易。更重要的是,C#的語言與 TypeScript 很相似,因此開發人員閱讀和編寫程式碼也非常輕鬆。 相反,Go 是一種完全不同的語言:沒有類別,沒有繼承,沒有例外,沒有套件層級的封裝(只有類別層級的封裝),而且語法也完全不同。當然這並不一定是壞事,但開發人員的確需要重新思考並用不同的方式設計程式碼,因此,同時使用 Go 和 TypeScript 是比較困難的。不過,Java 與 C#很相似,但依然缺乏許多 C#和 TypeScript 都有的功能。

2. C#和 TypeScript 的相似之處

也許你已經知道,C#和 TypeScript 有很多相似之處,如基於 C 的語法、類別、介面、泛型等。下面,我來詳細列舉一下二者的相似之處:

  • 2.1 async/await
  • 2.2 lambda 運算式和函數式陣列方法
  • 2.3 用於處理空的操作符(?,!,??)
  • 2.4 解構
  • 2.5 命令列界面(CLI)
  • 2.6 基本功能(類別、泛型、錯誤和列舉)

2.1 async/await

首先,C#和 JavaScript 都使用 async/await 來處理非同步程式碼。在 JavaScript 中,非同步操作以 Promise 表示,而應用程式可以 await 一個非同步操作結束。C#中的 Promise 其實是 Task,概念上與 Promise 完全相同,也有相應的方法。下面的例子示範了兩種語言中 async/await 的用法:

TypeScript 中 async/await 的例子:

async function fetchAndWriteToFile(url: string, filePath:string): Promise<string> {
  // fetch() returns aPromise
  const response = awaitfetch(url);
  const text = awaitresponse.text();
  // By the way, we'reusing Deno (https://deno.land)
  awaitDeno.writeTextFile(filePath, text);
  return text;
}

C#中 async/await 的例子:

using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

async Task<string> FetchAndWriteToFile(string url, stringfilePath) {
  // HttpClient.GetAsync()returns a Task
  var response = await newHttpClient().GetAsync(url);
  var text = awaitresponse.Content.ReadAsStringAsync();
  awaitFile.WriteAllTextAsync(filePath, text);
  return text;
}

下面是JavaScriptPromise API 與等價的C# Task API:

JavaScript API 等價的 C# API
Promise.all() Task.WaitAll()
Promise.resolve() Task.FromResult()
Promise.reject() Task.FromException()
Promise.prototype.then() Task.ContinueWith()
new Promise() new TaskCompletionSource()

2.2 Lambda 運算式和函數式陣列方法

C#JavaScript都用熟悉的=>語法(即箭頭函數)來表示lambda運算式。下面是TypeScriptC#的比較:

TypeScript 中使用 lambda 運算式:

const months = ['January', 'February', 'March', 'April'];
const shortMonthNames = months.filter(month => month.length< 6);
const monthAbbreviations = months.map(month =>month.substr(0, 3));
const monthStartingWithF = months.find(month => {
  returnmonth.startsWith('F');
});

C#中使用 lambda 運算式:

using System.Collections.Generic;
using System.Linq;

var months = new List<string> {"January","February", "March", "April"};
var shortMonthNames = months.Where(month => month.Length <6);
var monthAbbreviations = months.Select(month =>month.Substring(0, 3));
var monthStartingWithF = months.Find(month => {
  returnmonth.StartsWith("F");
});

上述示例示範了C#System.Linq命名空間中的一些方法,相當於JavaScript函數式陣列方法。下面是JavaScript陣列方法與等價的C# Linq方法:

JavaScript API 等價的 C# API
Array.prototype.filter() Enumerable.Where()
Array.prototype.map() Enumerable.Select()
Array.prototype.reduce() Enumerable.Aggregate()
Array.prototype.every() Enumerable.All()
Array.prototype.find() List.Find()
Array.prototype.findIndex() List.FindIndex()

2.3 處理空操作符

C#和 TypeScript 處理空的特性也一樣:

Feature name Syntax Documentation links
Optional properties property? TS :https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties,
C#:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-reference-types
Non-null assertion object!.property TS:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator,
C#:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving
Optional chaining object?.property JS :https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining,
C#:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-
Nullish coalescing object ?? alternativeValue JS:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator,
C#:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator

2.4 解構

儘管 C#預設不支援陣列或類別的解構,但它支援 Tuple 和 Record 的解構,使用者也可以為自訂型別定義解構。下面是 TypeScript 和 C#中解構的例子:

TypeScript 中解構的例子:

const author = { firstName: 'Kurt', lastName: 'Vonnegut' };
// Destructuring an object:
const { firstName, lastName } = author;

const cityAndCountry = ['Indianapolis', 'United States'];
// Destructuring an array:
const [city, country] = cityAndCountry;

C#中解構的例子:

using System;

var author = new Author("Kurt", "Vonnegut");
// Deconstructing a record:
var (firstName, lastName) = author;

var cityAndCountry = Tuple.Create("Indianapolis","United States");
// Deconstructing a tuple:
var (city, country) = cityAndCountry;

// Define the Author record used above
record Author(string FirstName, string LastName);

2.5 命令列界面(CLI)

我的開發方式是使用文字編輯器編寫程式碼,然後在終端執行命令,建置並執行。對於TypeScript,這意味著需要使用node或deno命令列界面(CLI)C#也有類似的CLI,名為dotnet(由 C#的.NET 執行時得名)。下面是使用dotnet CLI的一些例子:

mkdir app && cd app
# Create a new console application
# List of available app templates:https://docs.microsoft.com/dotnet/core/tools/dotnet-new
dotnet new console
# Run the app
dotnet run
# Run tests (don't feel bad if you haven't written those)
dotnet test
# Build the app as a self-contained
# single file application for Linux.
dotnet publish -c Release -r linux-x64

2.6 基本功能(類別、泛型、錯誤和列舉)

這些是 TypeScript 和 C#之間更基本的相似性。下面的例子是有關這幾個方面的介紹:

TypeScript 類別的示例:

import { v4 as uuidv4 } from'https://deno.land/std/uuid/mod.ts';

enum AccountType {
  Trial,
  Basic,
  Pro
}

interface Account {
  id: string;
  type: AccountType;
  name: string;
}

interface Database<T> {
  insert(item: T):Promise;
  get(id: string):Promise<T>;
}

class AccountManager {

  constructor(database:Database<Account>) {
    this._database =database;
  }

  asynccreateAccount(type: AccountType, name: string) {
    try {
      const account = {
        id: uuidv4(),
        type,
        name;
      };
      awaitthis._database.insert(account);
    } catch (error) {
        console.error(`Anunexpected error occurred while creating an account. Name: ${name}, Error:${error}`);
    }
  }

  private _database:Database<Account>;
}

C#類別的示例:

using System;
using System.Threading.Tasks;

enum AccountType {
  Trial,
  Basic,
  Pro
}

record Account(string Id, AccountType Type, string Name);

interface IDatabase<T> {
  Task Insert(T item);
  Task<T> Get(stringid);
}

class AccountManager {

  publicAccountManager(IDatabase<Account> database) {
    _database = database;
  }

  public async voidCreateAccount(AccountType type, string name) {
    try {
      var account = newAccount(
       Guid.NewGuid().ToString(),
        type,
        name
      );
      await_database.Insert(account)
    } catch (Exceptionexception) {
     Console.WriteLine($"An unexpected error occurred while creating anaccount. Name: {name}, Exception: {exception}");
    }
  }

  IDatabase<Account>_database;
}

3. C#的其他優勢

與 TypeScript 相似並不是 C#的唯一優點,它還有其他優點:

  • 3.1 與原生程式碼結合更容易
  • 3.2 事件
  • 3.3 其他功能

3.1 與原生程式碼結合

C#的最大優勢之一就是它可以深入原生程式碼。本文開頭提到,TypeScript不擅長與C/C++程式碼結合Node.js有一個支援原生C/C++的外掛,名為Node-API,但是它需要為原生函數編寫額外的C++包裹器,將原生型別轉換成JavaScript物件,或相反,類似於JNI的工作方式。而C#可以直接呼叫原生函數,只需把庫放到應用程式的 bin 目錄下,然後將 API 定義為 C#中的外部函數即可。然後就能像C#函數一樣使用外部函數,.NET執行時會處理好 C#資料型別與原生資料型別之間的轉換。例如,如果原生庫匯出了下面的 C 函數:

int countOccurrencesOfCharacter(char *string, char character) {    int count = 0;
    for (int i = 0;string[i] != '\0'; i++) {
        if (string[i] ==character) {
            count++;
        }
    }
    return count;
}

那麼可像下面這樣從 C#中呼叫:

using System;
using System.Runtime.InteropServices;

var count = MyLib.countOccurrencesOfCharacter("C# is prettyneat, eh?", 'e');
// Prints "3"
Console.WriteLine(count);

class MyLib {
    // Just placeMyLibraryName.so in the app's bin folder
   [DllImport("MyLibraryName")]
    public static externint countOccurrencesOfCharacter(string str, char character);
}

這種方法可以透過C連線訪問任何動態庫(.so.dll.dylib),也就是說,你可以輕鬆地呼叫CC++RustGo其他語言編寫的程式碼,只要編譯成機器碼即可。原生互動的其他應用還有:

  • 將指標作為 IntPtr 傳給原生物件
  • 利用GetFunctionPointerForDelegate()C#方法作為函數指標傳給原生函數
  • 使用Marshal.PtrToStringAnsi()C字串轉換為C#字串
  • 轉換結構和陣列

3.2 事件

C#的一個獨特的特性是,提供了一流的事件支援。在TypeScript中,你可以實現addEventListener()方法,讓客戶端監聽事件,而C#event關鍵字,可以用來定義事件,並透過簡單的語法將事件通知給所有監聽者(而不需要像TypeScript那樣手動遍歷所有事件監聽者並在try/catch區塊中執行)。例如,我們可以讓Connection類別定義一個MessageReceived事件,如下所示:

class Connection {
    // AnAction<string> is a callback that accepts a string parameter.
    public eventAction<string> MessageReceived;
}

使用Connection程式碼可以透過+=操作符給MessageReceived新增一個處理函數,如下:

var connection = new Connection();
connection.MessageReceived += (message) => {
   Console.WriteLine("Message was received: " + message);
};

Connection類別可以在內部呼叫MessageReceived,為所有監聽者觸發MessageReceived事件:

// Raise the MessageReceived event
MessageReceived?.Invoke(message);

4. 其他優勢

  1. 效能: C#很快。C#ASP.NET(Core) Web框架一直在Techempower的評測中名列前茅,而C#.NET CoreCLR執行時的效能每個主要版本都在提高。C#擁有優良效能的原因之一是,透過使用結構而不是類別,應用程式可以最小化甚至完全消除垃圾回收。因此,C#影片遊戲程式設計中非常流行。
  2. 遊戲和混合現實: C#是遊戲開發最流行的語言之一,像UnityGodot甚至Unreal遊戲引擎都使用了C#C#混合現實中也很流行,因為VRAR應用程式都是用Unity編寫的。
  3. 由於C#擁有第一方庫、工具和文件,因此一些任務非常容易實現,比如,在C#中建立gRPC客戶端要比TypeScript方便得多。相反,在Node.js中使用TypeScript時,就必須找出正確的模組和工具的組合,才能正確地生成JavaScript gRPC客戶端,以及相應的TypeScript型別。
  4. 進階功能:C#有許多其他語言沒有的功能,如運算子多載解構函式等。

5. 總結

如前所述,世上沒有完美的語言。在設計語言時總要有所權衡,所以一些語言的速度更快,但使用難度會增加(例如Rust的借出檢查)。另一方面,一些語言非常易用,但通常效能的優化難度就會增加(例如JavaScript的動態語言特性)。正因如此,我相信掌握一組相似的語言會非常有用:這些語言分別有各自的長處,但都很相似,而且能互相配合。例如,下面是我選擇的一組語言:

5.1 TypeScript

  • 最高層的語言,開發速度最快
  • 效能並非最佳,但適用於大多數應用
  • 不太適合與原生程式碼結合

5.2 C#

  • 仍然是高階語言,支援垃圾回收,所以很容易使用,儘管並不如TypeScript那麼容易。
  • 從速度和記憶體佔用量來看,其效能都優於 TypeScript
  • 最重要的是,能夠與底層很好地結合

C++

  • 開發難度較大(例如需要手動記憶體管理),因此開發速度會慢很多
  • 但執行時的效能最佳!而且隨處可用,能與許多已有的軟體相結合
  • 很像C#,而且標準庫很好,但也有許多陷阱(大多數與記憶體管理有關)。我更希望使用Rust,因為它的記憶體安全性更好,但我的許多工作都要與已有的C++程式碼結合,因此使用C++會更容易。

參考連結:https://nate.org/csharp-and-typescript

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2026/2/7

AOT使用經驗總結

從專案建立伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 AOT 發布測試。

繼續閱讀