在同事的專案採集到一枚奇特茶包。程式看似無誤,欄位也宣告成NVARCHAR,但塞入的Unicode難字硬是變亂碼,以下程式片段可重現問題:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.OracleClient;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Text;
 
namespace ConsoleApplication1
{
    class Program
    {
        static string cnStr =
        "Data Source=MyORACLE;User Id=user;Password=pwd;";
        static void Main(string[] args)
        {
            using (OracleConnection cn = new OracleConnection(cnStr))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = "INSERT INTO JEFFUNICODE VALUES (:t)";
                var p = cmd.Parameters.Add("t", SqlDbType.NVarChar);
                string s = "犇";
                p.Value = s;
                cmd.ExecuteNonQuery();
                cmd.CommandText = "SELECT * FROM JEFFUNICODE";
                cmd.Parameters.Clear();
                var dr = cmd.ExecuteReader();
                dr.Read();
                var t = dr["T"].ToString();
                Func<string, string> charCode = str => 
                    BitConverter.ToString(Encoding.UTF8.GetBytes(str));
                Debug.WriteLine(string.Format("{0}[{1}]->{2}[{3}]",
                    s, charCode(s), t, charCode(t)));
            }
        }
    }
}

執行結果為: 犇[E7-8A-87]->[EE-9B-95]

把眼睛睜大20%重新看一次程式,才發現裡面有個地方錯得離譜,明明是OracleCommand.Parameters.Add(),怎麼用SqlDbType.NVarChar? 要命的是還可以編譯可以執行?

修正為 var p = cmd.Parameters.Add("t", OracleType.NVarChar); ,一切就正常了。
犇[E7-8A-87]->犇[E7-8A-87]

整起事件離奇之處在於 -- 依System.Data.OracleClient.OracleParameterCollection的定義:

public OracleParameter Add(string parameterName, OracleType dataType);

為什麼傳入SqlDbType不會產生編譯錯誤? 強型別的C#接受了錯誤的型別,還可以編譯、執行,這一點都不科學啊! 難道,拎北這十幾年的.NET白學了? 眼睛再張大到50%(啊啊啊,眼角都撐到滲血了)重看一次定義才發現蹊蹺, Add()在兩個參數時還有一個Overloading:

public OracleParameter Add(string parameterName, object value);

原來,當第二個參數不是OracleType,SqlDbType.NVarChar被視為object,套用的是(string, object) Overloading,於是SqlDbType被誤當成OracleParameter.Value。以下程式可以證實:

var p = cmd.Parameters.Add("t", SqlDbType.NVarChar);
Console.WriteLine(p.DbType + "/" + p.Value.GetType());

結果為: Int32/System.Data.SqlDbType ,真相大白!

準備收工時才發現Visual Studio早給了提示: Parameters.Add()下方有條綠蚯蚓,提到.Add(string, object)已過時(deprecated)不建議使用,請改用AddWithValue()... 結果我還查到眼角流血才破案 XD

題外話,設計Overloading時要小心處理object型別,因為所有參數型別都可視為object,較容易套用錯誤發生非預期結果。過去開發時,看到有些Overloading在參數數量及順序做了特殊安排,起初覺得不自然,後來才意會到跟減少混淆誤用有關,是一種防呆設計,API的好壞就都在這些細節中囉~


Comments

# by

沒錯、所以 Oracle 贏是對的Orz

Post a comment