Закрепление типов значений с помощью GCHandle.Выделить

#c# #.net #c#-4.0 #garbage-collection #unsafe-pointers

Вопрос:

Можно ли закрепить передачу int ссылкой на метод, который затем закрепит эту переменную в памяти, чтобы я мог получить к ней доступ с помощью указателя?

Я пытаюсь использовать GCHandle.Выделите, чтобы временно закрепить int в памяти, чтобы я мог безопасно хранить указатель на это целое число. Когда я пытаюсь установить значение int с помощью указателя, хранящегося в GCHandle, мое целое число не обновляется. Если я создам указатель на int с помощью небезопасного кода, я смогу успешно изменить int. Похоже, что int передается по значению, и я устанавливаю другой int, чем тот, который на самом деле был закреплен. Вот пример проблемы, с которой я в настоящее время сталкиваюсь:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var count = 8001;
            var mc = MyClass.New(ref count);
            
            mc.Process();
        }
    }

    public unsafe sealed class MyClass
    {
        private GCHandle handle;
        private readonly int* countPtr;
        private MyClass(GCHandle handle, ref int totalCount)
        {
            this.handle = handle;
            fixed (int* p = amp;totalCount)
                countPtr = p;
        }

        public void Process()
        {
            var value = 9001;
            var handlePtr = (int*)handle.AddrOfPinnedObject();

            *handlePtr = (int)value; // count is unchanged
            *countPtr = (int)value; // IT'S OVER 9000!

            handle.Free();
        }

        public static MyClass New(ref int count)
        {
            var handle = GCHandle.Alloc(count, GCHandleType.Pinned);
            return new MyClass(handle, ref count);
        }
    }
}
 

Адреса памяти handlept и countPtr различны, что наводит меня на мысль, что то, что я считаю закрепленным, не закреплено… и я, возможно, создаю бомбу замедленного действия в своем приложении.

Пример Использования:

У меня есть пользовательский уровень доступа к данным, который использует следующий API

 public IEnumerable<Rows> CallMyProcedure(int pageIndex, int pageSize, out int totalCount)
{
    var rows = Call()
        .SetParameter("PageIndex", pageIndex)
        .SteParameter("PageSize", pageSize)
        .EnumerateDataSet(commandTimeoutSeconds: 60);
    
    return rows;
}
 

Вызов() начинает создавать контекст того, как хранимая процедура будет настроена во время выполнения. setParameter и несколько других методов добавляют данные в контекст, который будет использоваться при выполнении процедуры StoredProcedure. EnumerateData настраивает машину состояний IEnumerable, которая выполняет хранимую процедуру с помощью SqlDataReader, который выдает результаты в каждой строке.

Некоторые хранимые процедуры используют Смещение / выборку для получения ограниченного числа результатов. В этих случаях мне также нужно вывести общее количество строк, используя выходной параметр Sql

 public IEnumerable<Rows> CallMyProcedure(int pageIndex, int pageSize, out int totalCount)
{
    var count = 0;
    var rows = Call()
        .SetParameter("PageIndex", pageIndex)
        .SteParameter("PageSize", pageSize)
        .OutputTotalCount(x=> count = x)
        .EnumerateDataSet(commandTimeoutSeconds: 60)
        .ToList();
    
    totalCount = count;
    return rows;
}
 

Действие, которое я передаю, будет выполнено после перечисления всех строк и закрытия SqlDataReader, так как это единственный способ получить выходной параметр из хранимой процедуры SQL. Таким образом, единственная причина, по которой это работает, заключается в том, что я звоню .ТоЛист(). Если я не позвоню .ToList() значение out totalCount устанавливается равным 0, метод возвращается, и действие по установке количества срабатывает после считывания всех строк.

Я хочу добавить дополнительную перегрузку OutputTotalCount, чтобы иметь возможность использовать do

 public IEnumerable<Rows> GetAllData()
{
    var count = 0;
    return CallMyProcedure(1, int.MaxValue, out count);
}

public DataPage GetDataPage(int pageIndex, int pageSize)
{
    var count = 0;
    var list = CallMyProcedure(pageIndex, pageSize, out count).ToList()
    
    return new DataPage
    {
        Data = list,
        TotalCount = count
    }
}

public IEnumerable<Rows> CallMyProcedure(int pageIndex, int pageSize, out int totalCount)
{
    totalCount = 0; // Needed because we are leaving this method before setting
    var rows = Call()
        .SetParameter("PageIndex", pageIndex)
        .SteParameter("PageSize", pageSize)
        .OutputTotalCount(ref totalCount)
        .EnumerateDataSet(commandTimeoutSeconds: 60)
    
    return rows;
}
 

Новая перегрузка OutPutTotalCount здесь по существу добавляет экземпляр MyClass в контекст, который EnumerDataSet будет использовать для выполнения хранимой процедуры при вызове GetEnumerator. Процесс будет вызван после того, как все строки будут прочитаны. GCHandles также отслеживаются контекстом выполнения и являются свободными внутри try/finally.

Комментарии:

1. Я думаю, что целое число вставляется в вызов GCHandle. Alloc(), что означало бы, что вы правы, оно указывает на неправильное целое число.

2. Считаете ли вы, что он неправильно указывает на значение в коробке и на самом деле имеет закрепленное ожидаемое значение? Есть ли способ рассказать об этом с помощью VS2013 / Resharper?

3. Да, я думаю, что это правильно. Попробуйте сами боксировать int. object oi = i; var gcHandle = GCHandle.Alloc(oi, GCHandleType.Pinned); int *pi = (int*) gcHandle.AddrOfPinnedObject(); Затем *pi = 1 это должно быть отражено в oi.

4. Ваш GCHandle указывает на упакованную копию int. Но когда вы берете небезопасный указатель на int, этот указатель указывает на фактическое значение int в стеке. Таким образом, чтобы избежать небезопасных указателей, вам придется работать с int в штучной упаковке, который будет закреплен, как и ожидалось.

5. Знаете ли вы, что если count он выйдет за рамки, вы его потеряете? Опасно держать указатель на переменную в стеке, которая может жить дольше, чем область видимости. Я уверен, что он будет упакован в коробку, потому что он создаст копию в куче.