Подтвердить что ты не робот

Можете ли вы изменить содержимое (неизменяемой) строки с помощью небезопасного метода?

Я знаю, что строки неизменяемы, и любые изменения в строке просто создают новую строку в памяти (и маркируют старую как свободную). Тем не менее, мне интересно, звучит ли моя логика ниже в том, что вы на самом деле можете в круговом режиме изменить содержимое строки.

const string baseString = "The quick brown fox jumps over the lazy dog!";

//initialize a new string
string candidateString = new string('\0', baseString.Length);

//Pin the string
GCHandle gcHandle = GCHandle.Alloc(candidateString, GCHandleType.Pinned);

//Copy the contents of the base string to the candidate string
unsafe
{
    char* cCandidateString = (char*) gcHandle.AddrOfPinnedObject();
    for (int i = 0; i < baseString.Length; i++)
    {
        cCandidateString[i] = baseString[i];
    }
}

Действительно ли этот подход изменяет содержимое candidateString (без создания нового кандидатаString в памяти), или среда выполнения просматривает мои трюки и воспринимает ее как обычную строку?

4b9b3361

Ответ 1

Ваш пример отлично работает, благодаря нескольким элементам:

  • candidateString живет в управляемой куче, поэтому его можно модифицировать. Сравните это с baseString, который интернирован. Если вы попытаетесь изменить интернированную строку, могут возникнуть непредвиденные ситуации. Там нет гарантии, что строка не будет жить в защищенной от записи памяти в какой-то момент, хотя, похоже, она работает сегодня. Это было бы довольно похоже на назначение константной строки переменной char* в C, а затем ее модификацию. В C это поведение undefined.

  • Вы выделяете достаточное пространство в candidateString - поэтому вы не переполняете буфер.

  • Символьные данные не сохраняются со смещением 0 класса String. Он хранится со смещением, равным RuntimeHelpers.OffsetToStringData.

    public static int OffsetToStringData
    {
        // This offset is baked in by string indexer intrinsic, so there is no harm
        // in getting it baked in here as well.
        [System.Runtime.Versioning.NonVersionable] 
        get {
            // Number of bytes from the address pointed to by a reference to
            // a String to the first 16-bit character in the String.  Skip 
            // over the MethodTable pointer, & String 
            // length.  Of course, the String reference points to the memory 
            // after the sync block, so don't count that.  
            // This property allows C# fixed statement to work on Strings.
            // On 64 bit platforms, this should be 12 (8+4) and on 32 bit 8 (4+4).
    #if WIN32
            return 8;
    #else
            return 12;
    #endif // WIN32
        }
    }
    

    Кроме...

  • GCHandle.AddrOfPinnedObject специальный обернутый для двух типов: String и типы массивов. Вместо того, чтобы возвращать адрес самого объекта, он лежит и возвращает смещение к данным. См. исходный код в CoreCLR.

    // Get the address of a pinned object referenced by the supplied pinned
    // handle.  This routine assumes the handle is pinned and does not check.
    FCIMPL1(LPVOID, MarshalNative::GCHandleInternalAddrOfPinnedObject, OBJECTHANDLE handle)
    {
        FCALL_CONTRACT;
    
        LPVOID p;
        OBJECTREF objRef = ObjectFromHandle(handle);
    
        if (objRef == NULL)
        {
            p = NULL;
        }
        else
        {
            // Get the interior pointer for the supported pinned types.
            if (objRef->GetMethodTable() == g_pStringClass)
                p = ((*(StringObject **)&objRef))->GetBuffer();
            else if (objRef->GetMethodTable()->IsArray())
                p = (*((ArrayBase**)&objRef))->GetDataPtr();
            else
                p = objRef->GetData();
        }
    
        return p;
    }
    FCIMPLEND
    

Таким образом, время выполнения позволяет играть с его данными и не жалуется. Вы используете код unsafe в конце концов. Я видел худшее время выполнения во время выполнения, включая создание ссылочных типов в стеке; -)

Не забудьте добавить еще один \0 после всех символов (при смещении Length), если ваша окончательная строка короче выделенной. Это не будет переполняться, каждая строка имеет неявный нулевой символ в конце, чтобы облегчить сценарии взаимодействия.


Теперь посмотрим, как StringBuilder создает строку, здесь StringBuilder.ToString:

[System.Security.SecuritySafeCritical]  // auto-generated
public override String ToString() {
    Contract.Ensures(Contract.Result<String>() != null);

    VerifyClassInvariant();

    if (Length == 0)
        return String.Empty;

    string ret = string.FastAllocateString(Length);
    StringBuilder chunk = this;
    unsafe {
        fixed (char* destinationPtr = ret)
        {
            do
            {
                if (chunk.m_ChunkLength > 0)
                {
                    // Copy these into local variables so that they are stable even in the presence of race conditions
                    char[] sourceArray = chunk.m_ChunkChars;
                    int chunkOffset = chunk.m_ChunkOffset;
                    int chunkLength = chunk.m_ChunkLength;

                    // Check that we will not overrun our boundaries. 
                    if ((uint)(chunkLength + chunkOffset) <= ret.Length && (uint)chunkLength <= (uint)sourceArray.Length)
                    {
                        fixed (char* sourcePtr = sourceArray)
                            string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength);
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("chunkLength", Environment.GetResourceString("ArgumentOutOfRange_Index"));
                    }
                }
                chunk = chunk.m_ChunkPrevious;
            } while (chunk != null);
        }
    }
    return ret;
}

Да, он использует небезопасный код, и да, вы можете оптимизировать свою работу, используя fixed, поскольку этот тип пиннинга гораздо более легкий, чем выделение дескриптора GC:

const string baseString = "The quick brown fox jumps over the lazy dog!";

//initialize a new string
string candidateString = new string('\0', baseString.Length);

//Copy the contents of the base string to the candidate string
unsafe
{
    fixed (char* cCandidateString = candidateString)
    {
        for (int i = 0; i < baseString.Length; i++)
            cCandidateString[i] = baseString[i];
    }
}

Когда вы используете fixed, GC обнаруживает, что объект должен быть закреплен, когда он наткнулся на него во время сбора. Если сбор не происходит, GC даже не участвует. Когда вы используете GCHandle, дескриптор регистрируется в GC каждый раз.

Ответ 2

Как отмечали другие, мутация объектов String полезна в некоторых редких случаях. Я приведу пример с полезным фрагментом кода ниже.

Использование регистра/фон

Хотя каждый должен быть огромным поклонником действительно отличной поддержки Encoding, которую .NET всегда предлагал, иногда может быть предпочтительнее сократить эти накладные расходы, особенно если совершить много кругооборот между 8-разрядные (устаревшие) символы и управляемые строки (т.е. Типичные сценарии взаимодействия).

Как я и намекал, .NET особенно подчеркивает, что вы должны явно указать текст Encoding для любых/всех преобразований символьных данных, отличных от Юникода, в/из управляемых объектов String. Этот строгий контроль на периферии действительно заслуживает одобрения, поскольку он гарантирует, что после того, как у вас будет строка внутри управляемой среды выполнения, вам никогда не придется беспокоиться; все просто широкий Unicode. Даже UTF-8 в значительной степени изгнан в этом первозданном мире.

(Для сравнения, вспомните какой-то другой популярный язык сценариев, который лихо взвалил эту область, что в итоге привело к нескольким годам параллельных версий 2.x и 3.x, все из-за обширных изменений Unicode в последнем.)

Итак, .NET выталкивает весь этот беспорядок на границу взаимодействия, применяя Unicode (UTF-16), когда вы находитесь внутри, но эта философия влечет за собой работу с кодировкой/декодированием ( "раз и навсегда" ) ) должны быть исчерпывающе строгими, и из-за этого классы .NET Encoding/Encoder могут быть узким местом производительности. Если вы перемещаете много текста из широкого (Unicode) в простой фиксированный 7- или 8-битный узкий ANSI, ASCII и т.д. (Заметьте, я не говорю о MBCS или UTF-8, где вы захотите использовать кодеры!), парадигма кодирования .NET может показаться излишней.

Кроме того, может быть, что вы не знаете или не хотите указывать Encoding. Возможно, все, о чем вы заботитесь, это быстрое и точное круговое отключение для этого младшего байта 16-бит Char. Если вы посмотрите исходный код .NET, даже System.Text.ASCIIEncoding может быть слишком громоздким в некоторых ситуациях.


Фрагмент кода...

Тонкая строка: 8-разрядные символы, непосредственно хранящиеся в управляемом Строка, один "тонкий char" для широкого символа Юникода, без беспокоиться о кодировании/декодировании символов во время кругового отключения.

Все эти методы просто игнорируют/разделяют верхний байт каждого 16-разрядного символа Unicode, передавая только каждый младший байт в точности как-есть. Очевидно, что успешное восстановление текста Юникода после кругового путешествия будет возможно только в том случае, если эти верхние биты не имеют значения.

/// <summary> Convert byte array to "thin string" </summary>
public static unsafe String ToThinString(this byte[] src)
{
    int c;
    var ret = String.Empty;
    if ((c = src.Length) > 0)
        fixed (char* dst = (ret = new String('\0', c)))
            do
                dst[--c] = (char)src[c];  // fill new String by in-situ mutation
            while (c > 0);

    return ret;
}

В только что показанном направлении, которое обычно приводит к управлению встроенными данными в, у вас часто нет управляемого массива байтов, поэтому вместо выделения временного кода просто для вызова эта функция позволяет обрабатывать исходные байты непосредственно в управляемой строке. Как и раньше, это исключает все кодировки символов.

Для ясности исключены (очевидные) проверки диапазона, которые необходимы для этой небезопасной функции:

public static unsafe String ToThinString(byte* pSrc, int c)
{
    var ret = String.Empty;
    if (c > 0)
        fixed (char* dst = (ret = new String('\0', c)))
            do
                dst[--c] = (char)pSrc[c];  // fill new String by in-situ mutation
            while (c > 0);

    return ret;
}

Преимущество мутации String здесь заключается в том, что вы избегаете временных распределений путем прямой записи в окончательное распределение. Даже если вам нужно избегать дополнительного выделения с помощью stackalloc, было бы ненужное повторное копирование всего, когда вы в конечном итоге вызываете конструктор String(Char*, int, int): явно нет способа связать данные, которые вы просто кропотливо подготовили с помощью String объект, который не существовал, пока вы не закончили!


Для полноты...

Здесь зеркальный код, который отменяет операцию, чтобы вернуть массив байтов (даже если это направление не является иллюстрацией метода мутаций строк). Это направление, которое вы обычно использовали для отправки текста Unicode out управляемой среды выполнения .NET для использования устаревшим приложением.

/// <summary> Convert "thin string" to byte array </summary>
public static unsafe byte[] ToByteArr(this String src)
{
    int c;
    byte[] ret = null;
    if ((c = src.Length) > 0)
        fixed (byte* dst = (ret = new byte[c]))
            do
                dst[--c] = (byte)src[c];
            while (c > 0);

    return ret ?? new byte[0];
}