T-SQL与C#间Unicode字符映射为0xfdff的异常问题咨询
T-SQL与C#间Unicode字符映射问题:原因与解决方法
问题现象
T-SQL中的nvarchar字符串里,部分字符在C#中会被映射为0xfffd(Unicode替换字符),导致存回数据库时字符串损坏。例如0x4edc在T-SQL中可正常处理,但在C#中会被替换为0xfffd,复现测试代码如下:
[Test] public void DotNetUnicodeToSqlServerTest() { var unmappableCharacter = Encoding.Unicode.GetString(new byte[] { 0x4e, 0xdc }); var con = new SqlConnection(@"Server=(localdb)\mssqllocaldb;Database=unicode_test;Trusted_Connection=True;"); con.Open(); var cmd = con.CreateCommand(); cmd.CommandText = $"SELECT unicode(N'{unmappableCharacter}')"; var reader = cmd.ExecuteReader(); reader.Read(); var unicode = reader.GetInt32(0); // 无法映射为有效Unicode字符,被替换为替换字符 Assert.That(unicode, Is.EqualTo(0xfffd)); } [Test] public void SqlServerToDotNetTest() { var con = new SqlConnection(@"Server=(localdb)\mssqllocaldb;Database=unicode_test;Trusted_Connection=True;"); con.Open(); var cmd = con.CreateCommand(); cmd.CommandText = $"SELECT convert(nvarchar(10), 0x4edc), unicode(convert(nvarchar(10), 0x4edc))"; var reader = cmd.ExecuteReader(); reader.Read(); var unmappableCharacter = reader.GetSqlString(0).Value; // 无法映射为有效Unicode字符,被替换为替换字符 Assert.That(Encoding.Unicode.GetBytes(unmappableCharacter), Is.EqualTo(new byte[] { 0xfd, 0xff })); var unicode = reader.GetInt32(1); // T-SQL可正常处理该字符 Assert.That(unicode, Is.EqualTo(0xdc4e)); }
问题字符范围
在0x0000-0xffff范围内共有2050个字符存在此映射问题,这些字符的范围模式如下:
0x00D8-0x00E0 0x01D8-0x01E0 0x02D8-0x02E0 0x03D8-0x03E0 0x04D8-0x04E0 0x05D8-0x05E0 0x06D8-0x06E0 0x07D8-0x07E0 <<SNIP>>
编码测试结果
测试发现,Latin1编码可正常保留这类字符的原始字节,而默认UTF8编码会替换无效字符:
[Test] public void DotNetCanSupportProblemStringWithLatin1() { var unmappableCharacter = Encoding.Latin1.GetString(new byte[] { 0x4e, 0xdc }); var hex = "0x" + string.Join("", Encoding.Latin1.GetBytes(unmappableCharacter).Select(b => b.ToString("x2"))); Assert.That(hex, Is.EqualTo("0x4edc")); } [Test] public void DotNetCanSupportProblemStringWithDefaultUTF8() { var unmappableCharacter = Encoding.Default.GetString(new byte[] { 0x4e, 0xdc }); var hex = "0x" + string.Join("", Encoding.Default.GetBytes(unmappableCharacter).Select(b => b.ToString("x2"))); Assert.That(hex, Is.EqualTo("0x4eefbfbd")); }
当前环境
- SQL Server 2019
- C# 6
- 数据库排序规则:
SQL_Latin1_General_CP1_CI_AS
原因分析
- 编码规范差异:SQL Server的
nvarchar基于UCS-2/兼容UTF-16,但允许存储单独的代理项字符(0xD800-0xDFFF范围内的单个字符);而C#的string严格遵循UTF-16标准,要求代理项字符必须成对出现(高代理项+低代理项组合表示补充平面字符),单独的代理项会被视为无效字符,替换为0xfffd。 - 字符范围本质:问题字符均属于单独的低代理项(0xDC00-0xDFFF)或高代理项(0xD800-0xDBFF)范围,这类字符在UTF-16中是无效的孤立字符,但SQL Server允许存储。
解决方法
1. 清理无效字符(推荐)
从源头清理数据库中的孤立代理项字符,确保所有nvarchar存储的都是符合UTF-16标准的有效字符。示例T-SQL脚本:
-- 替换无效代理项为合法字符 UPDATE YourTable SET YourColumn = REPLACE(YourColumn, NCHAR(0xDC4E), N'替换字符') WHERE UNICODE(YourColumn) BETWEEN 0xD800 AND 0xDFFF;
2. 二进制方式传输
如果必须保留这些字符,通过varbinary类型在C#与SQL Server间传输,避免字符串编码转换:
- 读取时:将
nvarchar转换为varbinary,在C#中读取原始字节数组,不转换为字符串。 - 写入时:将字节数组作为
varbinary参数传入SQL Server,再转换为nvarchar存储。
3. 使用Latin1编码作为字节容器
利用Latin1编码(单字节编码,每个字节直接映射为字符)保留原始字节,避免替换:
// 从数据库读取时 var bytes = reader.GetSqlBytes(0).Buffer; var tempString = Encoding.Latin1.GetString(bytes); // 写入数据库时 var bytesToWrite = Encoding.Latin1.GetBytes(tempString); cmd.Parameters.Add("@data", SqlDbType.VarBinary).Value = bytesToWrite; cmd.CommandText = "INSERT INTO YourTable (YourColumn) VALUES (CONVERT(nvarchar(MAX), @data))";
4. 使用SqlBytes避免字符串转换
读取数据时直接获取SqlBytes而非SqlString,保留原始字节:
var sqlBytes = reader.GetSqlBytes(0); byte[] rawBytes = new byte[sqlBytes.Length]; sqlBytes.Read(0, rawBytes, 0, (int)sqlBytes.Length); // 写入时直接传递字节数组 cmd.Parameters.Add("@data", SqlDbType.VarBinary).Value = rawBytes;
内容的提问来源于stack exchange,提问作者Kram
相关产品推荐
相关产品推荐

