主頁 > .NET開發 > C# 9.0新特性詳解系列之五:記錄(record)和with運算式

C# 9.0新特性詳解系列之五:記錄(record)和with運算式

2020-12-10 09:25:42 .NET開發

1 背景與動機

傳統面向物件編程的核心思想是一個物件有著唯一標識,表現為物件參考,封裝著隨時可變的屬性狀態,如果你改變了一個屬性的狀態,這個物件還是原來那個物件,就是物件參考沒有因為狀態的改變而改變,也就是說該物件可以有很多種狀態,C#從最初開始也是一直這樣設計和作業的,但是一些時候,你可能非常需要一種恰好相反的方式,例如我需要一個物件只有一個狀態,那么原來那種默認方式往往會成為阻力,使得事情變得費時費力,

當一個型別的物件在創建時被指定狀態后,就不會再變化的物件,我們稱之為不可變型別,這種型別是執行緒安全的,不需要進行執行緒同步,非常適合并行計算的資料共享,它減少了更新物件會引起各種bug的風險,更為安全,System.DateTime和string就是不可變型別非常經典的代表,

原來,我們要用類來創建一個不可變型別,你首先要定義只讀欄位和屬性,并且還要重寫涉及相等判斷的方法等,在C#9.0中,引入了record,專門用來以最簡的方式創建不可變型別的新方式,如果你需要一個行為像值型別的參考型別,你可以使用record;如果你需要整個物件都是不可變的,且行為像一個值,那么你也可考慮將其宣告為一個record型別, 那么什么是record型別?

2 Record介紹

record型別是一種用record關鍵字宣告的新的參考型別,與類不同的是,它是基于值相等而不是唯一的識別符號——物件參考,他有著參考型別的支持大物件、繼承、多型等特性,也有著結構的基于值相等的特性,可以說有著class和struct兩者的優勢,在一些情況下可以用以替代class和struct,

提到不可變的型別,我們會想到readonly struct,那么為什么要選擇添加一個新的型別,而不是用readonly struct呢?這是因為記錄有著如下優點:

  • 在構造不可變的資料結構時,它的語法簡單易用,

  • record為參考型別,不用像值型別在傳遞時需要記憶體分配,并進行整體拷貝,

  • 建構式和結構函式為一體的、簡化的位置記錄

  • 有力的相等性支持,重寫了Equals(object), IEquatable, 和GetHashCode()這些基本方法,

2.1 record型別的定義與使用

2.1.1 常規方式

record型別可以定義為可變的,也可以是不可變的,現在,我們用record定義一個只有只讀屬性的Person型別如下,這種只有只讀屬性的型別,因為其在創建好之后,屬性就不能再被修改,我們通常把這種型別叫做不可變型別,

public record Person
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

上面這種宣告,在使用時,只能用帶參的建構式進行初始化,要創建一個record物件跟類沒有什么區別:

Person person = new("Andy", "Kang");

如果要支持用物件初始化器進行初始化,則在屬性中使用init關鍵字,這種形式,如果不需要用帶參的建構式進行初始化,可以不定義帶參的建構式,上面的Person可以改為下面形式,

public record Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

現在,創建Person物件時,用初始化器進行初始化如下:

Person person = new() { FirstName = "Andy", LastName = "Kang"};

如果需要是可變型別的record,我們定義如下,這種因為有set訪問器,所有它支持用物件初始化器進行初始化,如果你想用建構式進行初始化,你可以添加自己的建構式,

public record Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }

}

2.1.2 位置記錄 / Positional records

為了支持將record物件能解構成元組,我們給record添加解構函式Deconstruct,這種record就稱為位置記錄,下面代碼定義的Person,記錄的內容是通過建構式的引數傳入,并且通過位置解構函式提取出來,你完全可以在記錄中定義你自己的構造和解構函式(注意不是解構式),如下所示:,

public record Person 
{ 
    public string FirstName { get; init; } 
    public string LastName { get; init; }
    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);
    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}

針對上面如此復雜的代碼,C#9.0提供了更精簡的語法表達上面同樣的內容,需要注意的是,這種記錄型別是不可變的,這也就是為什么有record默認是不可變的說法由來,

public record Person(string FirstName, string LastName);

該方式宣告了公開的、僅可初始化的自動屬性、建構式和解構函式,現在創建物件,你就可以寫如下代碼:

var person = new Person("Mads", "Torgersen"); // 位置建構式
var (firstName, lastName) = person;                        // 位置解構函式

當然,如果你不喜歡產生的自動屬性、建構式和解構函式,你可以自定義同名成員代替,產生的建構式和解構函式將會只使用你自定義的那個,在這種情況下,被自定義引數處于你用于初始化的作用域內,例如,你想讓FirstName是個保護屬性:

public record Person(string FirstName, string LastName)
{
    protected string FirstName { get; init; } = FirstName; 
}

如上例子所示,對位置記錄進行擴展,你可以在大括號里添加你想要的任何成員,

一個位置記錄可以像下面這樣呼叫父類建構式,

public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);

2.1.3 定義的總結

record默認情況下是被設計用來進行描述不可變型別的,因此位置記錄這種短小簡明的宣告方式是推薦方式,

2.2 with運算式

當使用不可變的資料時,一個常見的模式是從現存的值創建新值來呈現一個新狀態,例如,如果Person打算改變他的姓氏(last name),我們就需要通過拷貝原來資料,并賦予一個不同的last name值來呈現一個新Person,這種技術被稱為非破壞性改變,作為描繪隨時間變化的person,record呈現了一個特定時間的person的狀態,為了幫助進行這種型別的編程,針對records就提出了with運算式,用于拷貝原有物件,并對特定屬性進行修改:

var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" };

如果只是進行拷貝,不需要修改屬性,那么無須指定任何屬性修改,如下所示:

Person clone = person with { };

with運算式使用初始化語法來說明新物件在哪里與原有物件不同,with運算式實際上是拷貝原來物件的整個狀態值到新物件,然后根據物件初始化器來改變指定值,這意味著屬性必須有init或者set訪問器,才能用with運算式進行更改,

需要注意的是:

  • with運算式左邊運算元必須為record型別,
  • record的參考型別的成員在拷貝的時候,只是將所指實體的參考進行了拷貝,

2.3 record的面向物件的特性——繼承、多型等

記錄(record)和類一樣,在面向物件方面,支持繼承,多型等所有特性,除過前面提到的record專有的特性,其他語法寫法跟類也是一樣,同其他型別一樣,record的基類依然是object,
要注意的是:

  • 記錄只能從記錄繼承,不能從類繼承,也不能被任何類繼承,

  • record不能定義為static的,但是可以有static成員,

下面一個學生record,它繼承自Person:

public record Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

public sealed record Student : Person
{
    public int ID { get; init; }
}

對于位置記錄,只要保持record特有的寫法即可:

public record Person(string FirstName, string LastName);

public sealed record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName);

public sealed record Teacher(string FirstName, string LastName, string Title) : Person(FirstName, LastName)
{
    public override string ToString()
    {
        StringBuilder s = new();
        base.PrintMembers(s);
        return $"{s.ToString()} is a Teacher";
    }
}

with運算式和值相等性與記錄的繼承結合的很好,因為他們不僅是靜態的已知型別,而且考慮到了整個運行時物件,比如,我創建一個Student物件,將其存在Person變數里,

Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };

with運算式仍然拷貝整個物件并保持著運行時的型別:

var otherStudent = student with { LastName = "Torgersen" };
WriteLine(otherStudent is Student); // true

同樣地,值相等性確保兩個物件有著同樣的運行時型別,然后比較他們的所有狀態:

Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 };
WriteLine(student != similarStudent); //true, 由于ID值不同

2.4 record實作原理

從本質上來講,record仍然是一個類,但是關鍵字record賦予這個類額外的幾個像值的行為,也就是,當你定義了record時候,編譯器會自動生成以下方法,來實作基于值相等的特性(即只要兩個record的所有屬性都相等,且型別相同,那么這兩個record就相等)、物件的拷貝和成員及其值的輸出,

  • 基于值相等性的比較方法,如Equals,==,!=,EqualityContract等,

  • 重寫GetHashCode()

  • 拷貝和克隆成員

  • PrintMembers和ToString()方法

例如我先定義一個Person的記錄型別:

public record Person(string FirstName, string LastName);

編譯器生成的代碼和下面的代碼定義是等價的,但是要注意的是,跟編譯器實際生成的代碼相比,名字的命名是有所不同的,

public class Person : IEquatable<Person>
{
    private readonly string _FirstName;
    private readonly string _LastName;

    protected virtual Type EqualityContract
    {
        get
        {
            return typeof(Person);
        }
    }

    public string FirstName
    {
        get
        {
            return _FirstName;
        }
        init
        {
            _FirstName = value;
        }
    }
    public string LastName
    {
        get
        {
            return _LastName;
        }
        init
        {
            _LastName = value;
        }
    }
    public Person(string FirstName, string LastName)
    {
        _FirstName = FirstName;
        _LastName = LastName;
    }

    public override string ToString()
    { 
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Person");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }

    protected virtual bool PrintMembers(StringBuilder builder)
    { 
        builder.Append("FirstName");
        builder.Append(" = ");
        builder.Append((object)FirstName);
        builder.Append(", ");
        builder.Append("LastName");
        builder.Append(" = ");
        builder.Append((object)LastName);
        return true;
    }

    public static bool operator !=(Person r1, Person r2)
    {
        return !(r1 == r2);
    }

    public static bool operator ==(Person r1, Person r2)
    {
        return (object)r1 == r2 || ((object)r1 != null && r1.Equals(r2));
    }

    public override int GetHashCode() 
    { 
        return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 
                + EqualityComparer<string>.Default.GetHashCode(_FirstName)) * -1521134295 
                + EqualityComparer<string>.Default.GetHashCode(_LastName);
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Person);
    }

    public virtual bool Equals(Person other)
    { 
        return (object)other != null 
                && EqualityContract == other.EqualityContract 
                && EqualityComparer<string>.Default.Equals(_FirstName, other._FirstName) 
                && EqualityComparer<string>.Default.Equals(_LastName, other._LastName);
    }

    public virtual Person Clone()
    { 
        return new Person(this);
    }

    protected Person(Person original)
    { 
        _FirstName = original._FirstName;
        _LastName = original._LastName;
    }
    public void Deconstruct(out string FirstName, out string LastName)
    { 
        FirstName = this.FirstName;
        LastName = this.LastName;
    }
}

這些由編譯器生成的一些成員,是允許編程人員自定義的,一旦編譯器發現有自定義的某個成員,它就不會再生成這個成員,

由此可見,record實際上就是編譯器特性,并且records由他們的內容來界定,不是他們的參考識別符號,從這一點上講,records更接近于結構,但是他們依然是參考型別,

2.4.1 基于值的相等

所有物件都從object型別繼承了 Equals(object),這是靜態方法 Object.Equals(object, object) 用來比較兩個非空引數的基礎,結構重寫了這個方法,通過遞回呼叫每個結構欄位的Equals方法,從而有了“基于值的相等”,Recrods也是這樣,這意味著只要他們的值保持一致,兩個record物件可以不是同一個物件實體就會相等,例如我們將修改的Last name又修改回去了:

var originalPerson = otherPerson with { LastName = "Nielsen" };

現在我們會得到 ReferenceEquals(person, originalPerson) = false (他們不是同一物件),但是 Equals(person, originalPerson) = true (他們有同樣的值).,與基于值的Equals一起的,還伴有基于值的GetHashCode()的重寫,另外,records實作了IEquatable并多載了==和 !=這兩個運算子,這些都是為了基于值的行為在所有的不同的相等機制方面保持一致,

基于值的相等和可變性契合的不總是那么好,一個問題是改變值可能引起GetHashCode的結果隨時變化,如果這個物件被存放在哈希表中,就會出問題,我們沒有不允許使用可變的record,但是我們不鼓勵那樣做,除非你已經想到了后果,

如果你不喜歡默認Equals重寫的欄位與欄位比較行為,你可以進行重寫,你只需要認真理解基于值的相等時如何在records中作業原理,特別是涉及到繼承的時候,

除了熟悉的Equals,==和!=運算子之外,record還多了一個新的EqualityContract只讀屬性,該屬性回傳型別是Type型別,回傳值默認為該record的型別,該屬性用來在判斷兩個具有繼承關系不同型別的record相等時,該record所依據的型別,下面我們看一個有關EqualityContract的例子,定義一個學生record,他繼承自Person:

public record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName);

這個時候,我們分別創建一個Person和Student實體,都用來描述同樣的人:

Person p = new Person("Jerry", "Kang");
Person s = new Student("Jerry", "Kang", 1);
WriteLine(p == s); // False

這兩者比較的結果是False,這與我們實際需求不相符,那么我們可以重寫EqualityContract來實作兩種相等:

public record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName)
{
    protected override Type EqualityContract
    {
        get => typeof(Person);
    }
}

經過此改造之后,上面例子中的兩個實體就會相等,EqualityContract的修飾符是依據下面情況確定的:

  • 如果基類是object, 屬性是virtual;
  • 如果基類是另一個record型別,則該屬性是override;
  • 如果基型別別是sealed,則該屬性也是sealed的,

2.4.2 拷貝克隆與with運算式

一個record在編譯的時候,會自動生成一個帶有保護訪問級別的“拷貝建構式”,用來將現有record物件的欄位值拷貝到新物件對應欄位中:

protected Person(Person original) { /* 拷貝所有欄位 */ } // 編譯器生成

with運算式就會引起拷貝建構式被呼叫,然后應用物件初始化器來有限更改屬性相應值,如果你不喜歡默認的產生的拷貝建構式,你可以自定義該建構式,編譯器一旦發現有自定義的建構式,就不會在自動生成,with運算式也會進行呼叫,

public record Person(string FirstName, string LastName)
{
    protected Person(Person original)
    {
        this.FirstName = original.FirstName;
        this.LastName = original.LastName;
    }
}

編譯器默認地還會生成with運算式會使用的一個Clone方法用于創建新的record物件,這個方法是不能在record型別里面自定義的,
2.4.3 PrintMembers和ToString()方法
如果你用Console.WriteLine來輸出record的實體,就會發現其輸出與用class定義的型別的默認的ToString完全不同,其輸出為各成員及其值組成的字串:

Person {FirstName = Andy, LastName = Kang}

這是因為,基于值相等的型別,我們更加關注于具體的值的情況,因此在編譯record型別時會自動生成重寫了ToString的行為的代碼,針對record型別,編譯器也會自動生成一個保護級別的PrintMembers方法,該方法用于生成各成員及其值的字串,即上面結果中的紅色字體部分,ToString中,就呼叫了PrintMembers來生成其成員字串部分,其他部分即藍色字體部分在ToString中補充,

我們也可以定義PrintMembers和重寫ToString方法來實作自己想要的功能,如下面實作ToString輸出為Json格式:

public record Person(string FirstName, string LastName)
{
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("\"FirstName\"");
        builder.Append(" : ");
        builder.Append($"\"{ FirstName}\"");
        builder.Append(", ");
        builder.Append("\"LastName\"");
        builder.Append(" : ");
        builder.Append($"\"{ LastName}\"");
        return true;
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("{");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }
}

record因為都是繼承自Object,因此ToString都是采用override修飾符,而PrintMembers方法修飾符是依據下面情況決定的:

  • 如果記錄不是sealed而是從object繼承的, 該方法是protected virtual;

  • 如果記錄基類是另一個record型別,則該方法是protected override;

  • 如果記錄型別是sealed,則該方法也是private的,

3 應用場景

3.1 Web Api

用于web api回傳的資料,通常作為一種一次性的傳輸型資料,不需要是可變的,因此適合使用record,

3.2 并發和多執行緒計算

作為不可變資料型別record對于并行計算和多執行緒之間的資料共享非常適合,安全可靠,

3.3 資料日志

record本身的不可變性和ToString的資料內容的輸出,不需要很多人工撰寫很多代碼,就適合進行日志處理,

3.4 其他

其他涉及到有大量基于值型別比較和復制的場景,也是record的常用的使用場景,

4 結束語

在生產應用中,有著眾多的使用場景,以便我們用record來替換寫一個類,未知的還在等我們進一步探索,

如對您有價值,請推薦,您的鼓勵是我繼續的動力,在此萬分感謝,關注本人公眾號“碼客風云”,享第一時間閱讀最新文章,

碼客風云

 

<iframe style="background: rgba(255, 255, 255, 1)" src="https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1612459507345899521&__biz=MzAwNjcyNTU2Ng==#wechat_redirect" frameborder="0" width="100%" height="342"></iframe>

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/232432.html

標籤:.NET技术

上一篇:C# 9.0新特性詳解系列之五:記錄(record)和with運算式

下一篇:C# 讀取outlook 本地簽名

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more