You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

原因分析

  1. 编码规范差异:SQL Server的nvarchar基于UCS-2/兼容UTF-16,但允许存储单独的代理项字符(0xD800-0xDFFF范围内的单个字符);而C#的string严格遵循UTF-16标准,要求代理项字符必须成对出现(高代理项+低代理项组合表示补充平面字符),单独的代理项会被视为无效字符,替换为0xfffd。
  2. 字符范围本质:问题字符均属于单独的低代理项(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 21:04:49