.NET 字串比對踩雷記
| | 1 | | 5,418 |
分享 .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 花時間爬文研究完才恍然大悟。
推薦幾篇文章:
- C# - Equals和等於等於(等於比較運算式) - 天空的垃圾場
- How to compare strings in C#
- Boxing(裝箱) 與 UnBoxing(拆箱) 測試 - .Net 蛤什麼?
- 自我測驗 - Value Type vs Reference Type
將爬文心得整理成以下規則:
- Value Type 型別轉型為 object 時會發生 Boxing
- string 是 Reference Type,轉型 object 不需要 Boxing
- object == object 比對背後其實是 ReferenceEqual,兩個變數都要指向相同個體時才算相等
- 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); }
- 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
考試用書「程式設計」的基本考題