分享 .NET 老鳥前陣子犯下的低級錯誤 - 字串比對結果與預想不同,還因觀念不清迷惑好一陣子(羞)。

故事是這樣的,先前知道 Dapper 查詢所傳回的 dynamic 底層型別是 DapperRow,並可轉型成 IDcitionary<string, object>方便動態指定欄位處理。(例如:跑迴圈巡覽所有欄位) 最近在寫跨資料表比對程式就用上這招,將兩筆資料都轉成 IDcitionary<string, object>,用 data1[colName] == data2[colName] 比對。 遇到古怪情況,明明二者都是字串且內容相同,但結果卻是不等於。

轉成 IDcitionary<string, object> 時,字串值被視為 object 處理,會是 Boxing 造成的?

於是我做了以下實驗:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Dynamic;
using System.Linq;
using Dapper;

namespace ConsoleApp1
{
    class Program
    {
        static string cs = "data source=localhost;integrated security=SSPI";
        static void Main(string[] args)
        {
            string s1 = "Jeffrey", s2 = "Jeffrey";
            int i1 = 123, i2 = 123;
            object os1 = s1, os2 = s2; //string 是 Refence Type,不需 Boxing
            object oi1 = i1, oi2 = i2; //int 是 Value Type,需要 Boxing
            Console.WriteLine($"os1 == os2 => {os1 == os2}");
            Console.WriteLine($"oi1 == oi2 => {oi1 == oi2}");
            var eo = new ExpandoObject();
            var d = eo as IDictionary<string, object>;
            d["s1"] = s1;
            d["s2"] = s2;
            d["i1"] = i1;
            d["i2"] = i2;
            Console.WriteLine($"d['s1'] == d['s2'] => {d["s1"] == d["s2"]}");
            Console.WriteLine($"d['i1'] == d['i2'] => {d["i1"] == d["i2"]}");
            using (var cn = new SqlConnection(cs))
            {
                var res = cn.Query("SELECT 'Jeffrey' as s1, 'Jeffrey' as s2").First();
                Console.WriteLine($"res.s1 == res.s2 => {res.s1 == res.s2}");
                d = res as IDictionary<string, object>;
                Console.WriteLine($"Names = {string.Join(",", d.Keys)}");
                Console.WriteLine($"d['s1'] == d['s2'] => {d["s1"] == d["s2"]}");
            }
            Console.ReadLine();
        }
    }
}

測試結果如下:

定義 i1, i2 兩個數值相同的整數,轉成兩個 object oi1 與 oi2,oi1 == oi2 比對不相等。
定義 s1, s2 兩個內容相同的字串,轉成兩個 object os1 與 os2,os1 == os2 比對相等。
建立 IDcitionary<string, object>,放入 s1, s2, i1, i2, d["s1"] == d["s2"] 比對相等,d["i1"] == d["i2"] 不相等。
使用 Dapper 查詢取回內容相同的兩個字串欄位 s1, s2,直接比對 dynamic 的 s1 與 s2 屬性相等。 將 dynamic 轉型成 IDcitionary<string, object> 後 d["s1"] == d["s2"] 卻不相等。

觀念不夠清楚,做完實驗更迷糊 Orz 花時間爬文研究完才恍然大悟。

推薦幾篇文章:

將爬文心得整理成以下規則:

  1. Value Type 型別轉型為 object 時會發生 Boxing
  2. string 是 Reference Type,轉型 object 不需要 Boxing
  3. object == object 比對背後其實是 ReferenceEqual,兩個變數都要指向相同個體時才算相等
  4. string 實作了 == 運算子,故 string == string 背後是呼叫 string.Equals(a, b) 比對字串內容
     /// <summary>Determines whether two specified strings have the same value.</summary>
     /// <returns>true if the value of <paramref name="a" /> 
     ///  is the same as the value of <paramref name="b" />; otherwise, false.</returns>
     /// <param name="a">The first string to compare, or null. </param>
     /// <param name="b">The second string to compare, or null. </param>
     /// <filterpriority>3</filterpriority>
     [__DynamicallyInvokable]
     public static bool operator ==(string a, string b)
     {
         return string.Equals(a, b);
     }   
    
  5. C# 編譯器在處理 string s1 = "Jeffrey", s2 = "Jeffrey" 指定字串常數到變數動作時,依據 String Interning 機制會共用相同字串內容, 因此 s1 與 s2 將指向同一個記憶體位址。

掌握以上原則,一切有了合理解釋。

oi1 與 oi2 是 int 被轉型成 object,會發生 Boxing 變成兩個 object 個體[規則1],oi1 == oi2 結果為不同等[規則3]。 s1 與 s2 因為 String Interning 指向相同記憶體位址[規則5],故 os1 == os2 因地址相同結果為相等[規則3]。 自建 IDcitionary<string, object>,放入 s1, s2, i1, i2,一樣是將 int 及 string 轉成 object。 故 d["s1"] == d["s2"]、d["i1"] == d["i2"] 也與 s1 == s2、i1 == i2 一致。
Dapper 取回結果之 res.s1、res.s2 型別均為 string,故 res.s1 == res.s2 背後為 string.Equals(),故結果相等。[規則4]
最後,轉型為 IDcitionary<string, object> 後,d["s1"] 與 d["s2"] 是 object == object, 而背後的 string 物件來自資料庫查詢是動態產生的不會有 String Interning,即使內容相同記憶體位址也不同,故為不相等。[規則3]

都解釋清楚了,那來個隨堂測驗,假設程式如下:

static void Main(string[] args)
{
    string s1 = "A", s2 = "A";
    object os1 = s1, os2 = s2;
    Console.WriteLine(os1 == os2);
    os1 = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(s1));
    Console.WriteLine(os1 == os2);
    Console.WriteLine((string)os1 == (string)os2);
    Console.WriteLine(os1.ToString() == os2.ToString());
    Console.ReadLine();
}

執行結果為:

總共有四組 os1 os2 比較,結果分別為 true, false, true, true,大家能解釋為什麼嗎?

解答:規則5、規則3、規則4、規則4

Tips of strings comparing when they are casting to objects.


Comments

# by Huang

考試用書「程式設計」的基本考題

Post a comment