重用 SqlDataRecord 是否安全?

Is it safe to reuse a SqlDataRecord?

在实现 table 值参数时,生成供参数使用的 IEnumerable<SqlDataRecord> 的最常见方法之一是这样的代码(例如 ):

public static IEnumerable<SqlDataRecord> Rows(List<int> simpletable)
{
    var smd = new []{ new SqlMetaData("id", SqlDbType.Int)};
    var sqlRow = new SqlDataRecord(smd);
    foreach (int i in simpletable)
    {
        sqlRow.SetInt32(0, i);
        yield return sqlRow;
    }
}
//...
var param = sqlCmd.Parameters.AddWithValue("@retailerIDs", Rows(mydata)); 
param.SqlDbType = SqlDbType.Structured;
param.TypeName = "myTypeName";

这段代码似乎确实有效。虽然重用 SqlMetaData 不会引起太多警钟,但在 foreach 循环之外声明 SqlDataRecord 对我来说非常可疑:

一个mutable对象被修改然后重复yield

举个例子说明为什么会这样,在 LinqPad 中调用 var x = Rows(new[] { 100, 200}.ToList()).ToList().Dump() 会输出 200,200。这种方法似乎依赖于实现细节(单独处理行),但我没有看到任何承诺这一点的文档。

是否有一些缓解因素可以使这种方法安全?

如果您在 foreach 循环之外根本不需要它,我不明白您为什么要重新使用它。

我发现这个问题 Is there a reason for C#'s reuse of the variable in a foreach? which links to this answer in another question Is it better coding practice to define variables outside a foreach even though more verbose? Jon Skeet 回答说:

There's no advantage to declaring the variables outside the loop, unless you want to maintain their values between iterations.

(Note that usually this makes no behavioural difference, but that's not true if the variables are being captured by a lambda expression or anonymous method.)

不,重复使用变量是不安全的。代码一遍又一遍地修改同一个对象。这是错误的代码,应该返回一个新对象。这是一个快速的 linqpad 示例,显示了上述代码的问题:

void Main()
{
    //This code proves that the object is being modified.
   Thing prevRow = null;
    foreach (var curRow in Rows(new List<int>() { 1, 2, 3 }))
    {
        Console.WriteLine(curRow);
        Console.WriteLine(prevRow);
        prevRow = curRow;
    }

    //Because the object is modified instead of a new object being returned,
    // this code does something unexpected; it returns the same object 3
    // times! Instead of three unique objects representing the values 1, 2, 3.
    var rowsAsList = Rows(new List<int>() { 1, 2, 3 }).ToList();
    foreach (var curRow in rowsAsList)
    {
        Console.WriteLine(curRow);
    }
}

public class Thing
{
    public int i;
}

IEnumerable<Thing> Rows(List<int> simpletable)
{
    var sqlRow = new Thing() {i=-1};
    foreach (int i in simpletable)
    {
        sqlRow.i = i;
        yield return sqlRow;
    }
}

This approach seems to rely on an implementation detail (that rows are processed individually), but I don't see any documentation which promises this.

Is there some mitigating factor which renders this approach safe?

正如 user1249190 指出的那样,https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.server.sqldatarecord#remarks 的备注部分明确推荐重用 SQLDataRecord:

This class is used together with SqlPipe to send result sets to the client from managed code stored-procedures. When writing common language runtime (CLR) applications, you should re-use existing SqlDataRecord objects instead of creating new ones every time. Creating many new SqlDataRecord objects could severely deplete memory and adversely affect performance.

显然此建议不适用于跨线程使用:文档还明确警告 "Any instance members are not guaranteed to be thread safe."