Объявите указатель на свойство и передайте его как параметр inout в функцию в Swift?

#swift #parameters #reference #unsafemutablepointer #inout

Вопрос:

Мне нужно выбрать одно из некоторых свойств и передать его по ссылке, чтобы установить его внутри функции. Приблизительный код:

 var someProperty = [SomeClass]()
var someProperty2 = [SomeClass]()

func someFunc(someObject: inout [SomeClass]) {
    ...
    someObject = ... //
}

//usage code
let obj: [SomeClass]
if someCase {
    obj = self.someProperty
    ...
} else {
    obj = self.someProperty2
    ...
}
someFunc(amp;obj)
 

Проблема obj в том, что is нельзя использовать в качестве inout параметра, но даже если я объявлю его как var тогда obj , он будет изменен, но не someProperty или someProperty2 .

Я читал, что как-то мне нужно объявить obj , как UnsafeMutablePointer и есть подобные вопросы. Но я не знаю, как применить их к приведенному выше коду, чтобы исправить только эти 3 строки (без изменения остальной части кода).:

 let obj: [SomeClass]
obj = self.someProperty
obj = self.someProperty2
 

Как решить эту проблему?

P.S. другими словами, мне нужно что-то вроде let obj: inout [SomeClass] , но это не разрешено Swift

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

1. @JoakimDanielson Immutable value 'obj' must not be passed inout

2. Рассматривали ли вы возможность использования WritableKeyPath вместо UnsafeMutablePointer? Ключевые пути являются типобезопасными и высокоуровневыми, в отличие от указателей.

3. @JoakimDanielson Как я это проверяю — я создаю копию someProperty , изменяю ее и пытаюсь вернуть обратно внутрь someFunc . Результат obj меняется, но не someProperty меняется .

4. @DanZheng не могли бы вы опубликовать пример с моим кодом, пожалуйста? Может быть, это решит мою проблему

Ответ №1:

После обсуждения в комментариях вопрос должен быть конкретно о том, как работать UnsafeMutablePointer для достижения результата, а не о том, как лучше всего достичь результата.

Важно, чтобы весь код от момента получения указателя до его использования был ограничен областью действия withUnsafeMutablePointer . Поскольку все сводится к выбору между двумя массивами, а затем передаче одного из них через псевдоним someFunc , вы не знаете, какой указатель должен быть сохранен в режиме реального времени, поэтому вам нужно сохранить их оба в режиме реального времени. В противном случае ваша программа, скорее всего, выйдет из строя, когда Swift аннулирует ее.

Правильный и безопасный способ достижения желаемого эффекта с помощью указателей выглядит следующим образом:

 func pointerSolution()
{
    var sourceArray1 = [1, 2, 3, 4]
    var sourceArray2 = [5, 6]

    func someFunc(_ someArray: inout [Int]) {
        someArray.indices.forEach { someArray[$0] = 0 }
    }
    
    let someCase = true // just for the sake of compiling a concrete case

    //usage code
    withUnsafeMutablePointer(to: amp;sourceArray1)
    { src1 in
        withUnsafeMutablePointer(to: amp;sourceArray2)
        { src2 in
            // Substitute appropriate type for `Int`
            let arrayPtr: UnsafeMutablePointer<[Int]>
            if someCase {
                arrayPtr = src1
                // Some additional code that may or may not involve arrayPtr
            } else {
                arrayPtr = src2
                // Some additional code that may or may not involve arrayPtr
            }
            someFunc(amp;arrayPtr.pointee)
        }
    }

    print("sourceArray1 = (sourceArray1)")
    print("sourceArray2 = (sourceArray2)")
}
 

Если вам нужно сделать это в нескольких местах или просто хотите очистить синтаксическое раздувание вложенных withUnsafeMutablePointer блоков, вы можете предоставить вспомогательную функцию:

 func withUnsafeMutablePointers<T, R>(
    to value1: inout T, 
    and value2: inout T, 
    _ body: (UnsafeMutablePointer<T>, UnsafeMutablePointer<T>) throws -> R) rethrows -> R
{
    try withUnsafeMutablePointer(to: amp;value1)
    { ptr1 in
        try withUnsafeMutablePointer(to: amp;value2)
        { ptr2 in
            try body(ptr1, ptr2)
        }
    }
}
 

Тогда там, где вы его используете, у вас есть один уровень вложенности:

 func pointerSolution()
{
    var sourceArray1 = [1, 2, 3, 4]
    var sourceArray2 = [5, 6]

    func someFunc(_ someArray: inout [Int]) {
        someArray.indices.forEach { someArray[$0] = 0 }
    }
    
    let someCase = true // just for the sake of compiling a concrete case

    //usage code
    withUnsafeMutablePointers(to: amp;sourceArray1, and: amp;sourceArray2)
    { src1, src2 in
        // Substitute appropriate type for `Int`
        let arrayPtr: UnsafeMutablePointer<[Int]>
        if someCase {
            arrayPtr = src1
            // Some additional code that may or may not involve arrayPtr
        } else {
            arrayPtr = src2
            // Some additional code that may or may not involve arrayPtr
        }
        someFunc(amp;arrayPtr.pointee)
    }

    print("sourceArray1 = (sourceArray1)")
    print("sourceArray2 = (sourceArray2)")
}
 

Если вы хотите жить опасно, вы можете сделать это:

 func dangerousPointerSolution()
{
    var sourceArray1 = [1, 2, 3, 4]
    var sourceArray2 = [5, 6]

    func someFunc(_ someArray: inout [Int]) {
        someArray.indices.forEach { someArray[$0] = 0 }
    }

    let someCase = true // just for the sake of compiling a concrete case

    //usage code
    let address: Int
    if someCase {
        address = withUnsafeMutablePointer(to: amp;sourceArray1) { Int(bitPattern: $0) }
        // Some additional code that may or may not involve address
    } else {
        address = withUnsafeMutablePointer(to: amp;sourceArray2) { Int(bitPattern: $0) }
        // Some additional code that may or may not involve address
    }
    someFunc(amp;(UnsafeMutablePointer<[Int]>(bitPattern: address)!).pointee)

    print("sourceArray1 = (sourceArray1)")
    print("sourceArray2 = (sourceArray2)")
}
 

Обратите внимание на преобразование указателя до конца Int . Это связано с тем, что при withUnsafeMutablePointer возврате он делает указатель недействительным $0 внутренне, и если вы просто вернетесь $0 , указатель, который возвращается, withUnsafeMutablePointer также будет признан недействительным. Поэтому вам нужно хитростью Swift заставить вас дать что-то, что вы можете использовать за пределами withUnsafeMutablePointer . Преобразуя его в an Int , вы в основном сохраняете действительный адрес в виде числового значения. Свифт не может признать это недействительным. Затем снаружи withUnsafeMutablePointer вы должны преобразовать этот Int адрес обратно в указатель, который UnsafeMutablePointer<T> имеет инициализатор (в конце концов, вы можете представить встроенную систему с отображением ввода-вывода в память. Вам нужно будет читать/записывать по определенному адресу для ввода-вывода.) Каждый раз, когда вам нужно обмануть компилятор, чтобы он позволил вам что-то сделать, это должно быть большим красным флагом, что, возможно, вам не следует этого делать. У вас все еще могут быть веские причины, но, по крайней мере, это должно заставить вас усомниться в них и рассмотреть альтернативы.

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

Короче говоря, при использовании указателей сохраняйте их как можно более локальными и убедитесь, что они или что-либо, что может быть использовано для их восстановления без исходного «объекта» Swift, на который они указывают, не выходят за рамки контекста, в котором вы точно знаете, что они действительны. Это не C. У вас нет такого большого контроля над временем жизни выделенной памяти. Обычно в Swift вам не нужно беспокоиться об этом, но когда вы используете указатели, на самом деле сложнее рассуждать об их действительности, чем в C, по той самой причине, что вы не можете указать, когда выделенная память становится недействительной. Например, в Swift не гарантируется, что локально выделенный экземпляр класса останется «живым» до конца области видимости. На самом деле, он часто деинициализируется сразу после его последнего использования, даже если после этого в той же области может быть больше кода. Если у вас есть указатель на такой объект, даже если вы все еще находитесь в той же области, теперь вы можете указывать на деинициализированную память. Swift даже должен обеспечить withExtendedLifetime , чтобы иметь дело с такими случаями. Вот почему Swift пытается ограничить их использование исключительно в рамках withUnsafePointer семейства функций. Это единственный контекст, в котором он может гарантировать их действительность. Существуют и другие контексты, в которых они были бы допустимы, но компилятор не может доказать, что это так.

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

1. Кажется, что первый способ-это почти то же самое, что мне нужно, но я также хочу попробовать позвонить let arrayPtr: UnsafeMutablePointer<[Int]> if someCase { извне withUnsafeMutablePointer . В этом случае я также мог бы обернуть someFunc(amp;arrayPtr.pointee) внутри блока.

2. Да, вы можете это сделать, но это должно быть var arrayPtr: UnsafeMutablePointer<[Int]>! (неявно развернуто необязательно), иначе вы получите ошибку компилятора о его захвате до его инициализации. И, конечно, вам нужно будет убедиться, что все виды использования arrayPtr остаются ограниченными withUnsafeMutablePointer закрытиями.

3. Я добавил предложение для вспомогательной функции для переноса двойных withUnsafeMutablePointer вызовов. Если вам нужно сделать это только один раз, двойная вложенность на сайте использования не так уж плоха, но это может раздражать, если вам придется делать это в нескольких других местах или, что еще хуже, если вам нужно расширить метод, чтобы обрабатывать более двух указателей. В этом случае наличие вспомогательной функции делает сайт намного чище.

Ответ №2:

Нашел решение, но оно выглядит странно:

 var someProperty = [SomeClass]()
var someProperty2 = [SomeClass]()

func someFunc(someObject: inout [SomeClass]) {
    ...
    someObject = ... //
}

//usage code
let obj: UnsafeMutablePointer<[SomeClass]>
if someCase {
    obj = withUnsafeMutablePointer(to: amp;self.someProperty) { $0 }
    ...
} else {
    obj = withUnsafeMutablePointer(to: amp;self.someProperty2) { $0 }
    ...
}
someFunc(amp;obj.pointee)
 

Таким образом, изменяется только раздел «код использования». Некоторое время я не находил правильного решения, потому что, например, следующий код выдает предупреждение «висящий указатель» :

 obj = UnsafeMutablePointer<[SomeClass]>(amp;self.someProperty)
 

Также мое решение иногда вызывает эту проблему:

Thread 1: Simultaneous accesses to 0x7f85df805fc0, but modification requires exclusive access

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

1. Но действительно ли вам нужно это сложное решение, которое иногда не работает? Вероятно, это означает написание большего количества кода, но почему бы не использовать переменные напрямую, someFunc(someObject: amp;somePropery) . Больше кода, менее элегантного, но, может быть, и менее хлопотного?

2. Я знаю по опыту, что у этого кода есть хорошие шансы на сбой. withUnsafeMutablePointer Функции Swift делают указатель недействительным при выходе, поэтому возврат $0 проблематичен. Чтобы заставить его работать, вы должны преобразовать его в an Int , чтобы в основном сохранить действительный адрес в указателе, а затем на принимающей стороне преобразовать Int его в изменяемый указатель.

3. Чтобы это работало безопасно, вам нужно будет вложить if и позвонить someFunc во вложенные вызовы в withUnsafeMutablePointer вызовы. Таким образом, указатели на someProperty и someProperty2 остаются действительными на протяжении всего вызова someFunc .

4. @ChipJarred В общем, вопрос в том, как работать с указателями в Swift, такими как objc, но вы просто предлагаете избегать таких ситуаций. мне это не подходит. Если это вообще невозможно, то просто скажите об этом

5. @VyachaslavGerchicov Если ваш вопрос на самом деле касается указателей, а не того, как достичь результата, то вам следует пересмотреть свой вопрос, чтобы сказать об этом. Мой предыдущий комментарий в этой теме дает ответ, как это сделать безопасно. Вы должны сохранять указатели действительными, что означает вложенный withUnsafeMutablePointer код. Это некрасиво и не должно проходить проверку кода для этого случая ни в одной организации.

Ответ №3:

По-видимому, я изначально был тусклым в своем прочтении вопроса, поэтому с обновленным (и, надеюсь, точным) пониманием я переформулирую вопрос, предоставив конкретный (и компилируемый) код того, что, по моему мнению, хочет сделать ОП, а затем дам свое решение.

Я переименовал несколько вещей, чтобы быть более конкретным и выразить их роли в коде. Данный код выглядит следующим образом:

 var sourceArray1 = [1, 2, 3, 4]
var sourceArray2 = [5, 6, 7, 8]

func someFunc(_ someArray: inout [Int]) {
    someArray.indices.forEach { someArray[$0] = 0 }
}

let someCase = true // just for the sake of compiling a concrete case

//usage code
var arrayProxy: [Int]
if someCase {
    arrayProxy = sourceArray1
    // Some additional code that may or may not involve arrayProxy
} else {
    arrayProxy = sourceArray2
    // Some additional code that may or may not involve arrayProxy
}
someFunc(amp;arrayProxy)

print("sourceArray1 = (sourceArray1)")
print("sourceArray2 = (sourceArray2)")
 

фактический объем производства составляет

 sourceArray1 = [1, 2, 3, 4]
sourceArray2 = [5, 6, 7, 8]
 

Но желаемый результат-это

 sourceArray1 = [0, 0, 0, 0]
sourceArray2 = [5, 6, 7, 8]
 

Таким образом, какой бы исходный массив ни был выбран if оператором , он является массивом, подлежащим изменению, и arrayProxy , хотя он может быть использован в более позднем коде, не имеет значения для целей желаемого эффекта здесь.

Я не думаю, что указатели здесь являются правильным решением. Я думаю, что небольшая модификация дизайна была бы лучше. Что желательно, так это изменить один из исходных массивов, так почему бы просто не сделать это? Мое решение было бы таким:

 var sourceArray1 = [1, 2, 3, 4]
var sourceArray2 = [5, 6, 7, 8]

func someFunc(_ someArray: inout [Int]) {
    someArray.indices.forEach { someArray[$0] = 0 }
}

func caseTrue(_ someArray: inout [Int]) -> [Int]
{
    // The additional code from the if statement's `true` branch
    someFunc(amp;someArray)
    return someArray
}

func caseFalse(_ someArray: inout [Int]) -> [Int] {
    // The additional code from the if statement's `false` branch
    someFunc(amp;someArray)
    return someArray
}

let someCase = true // just for the sake of compiling a concrete case

//usage code - assuming arrayProxy is still needed for something in later code
let arrayProxy = someCase ? caseTrue(amp;sourceArray1) : caseFalse(amp;sourceArray2)

print("sourceArray1 = (sourceArray1)")
print("sourceArray2 = (sourceArray2)")
 

caseTrue и caseFalse могут быть локальными функциями, вложенными в метод, содержащий if , чтобы они не загрязняли код вне текущего контекста.

 class AClass
{
    var sourceArray1 = [1, 2, 3, 4]
    var sourceArray2 = [5, 6, 7, 8]
    
    init() { }
    
    func someFunc(_ someArray: inout [Int]) {
        someArray.indices.forEach { someArray[$0] = 0 }
    }

    func aMethod()
    {
        func caseTrue(_ someArray: inout [Int]) -> [Int]
        {
            // The additional code from the if statement's `true` branch
            someFunc(amp;someArray)
            return someArray
        }

        func caseFalse(_ someArray: inout [Int]) -> [Int] {
            // The additional code from the if statement's `false` branch
            someFunc(amp;someArray)
            return someArray
        }
        
        let someCase = true // just for the sake of compiling a concrete case

        //usage code - assuming arrayProxy is still needed for something in later code
        let arrayProxy = someCase ? caseTrue(amp;sourceArray1) : caseFalse(amp;sourceArray2)

        print("sourceArray1 = (sourceArray1)")
        print("sourceArray2 = (sourceArray2)")
    }
}
 

Если тела caseTrue и caseFalse зависят от локальных переменных aMethod , это не проблема. Локальные функции захватывают переменные из окружающего контекста точно так же, как замыкания (на самом деле в Swift они в основном просто называются замыканиями). Поэтому объявите функции непосредственно перед тем, как вам нужно будет их вызвать, что означает, что внутри них может быть какой-то код над ними aMethod .

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

1. «вам не нужен вывод» , это верно только в том случае, если вы хотите изменить только элементы в массиве, но не в том случае, если вы хотите изменить сам массив

2. Итак, хорошо, обозначение скобок действительно означает Array<SomeClass> , а не просто ваш способ сказать «замените тип класса здесь».?

3. Также , если вы передадите массив как inout , а затем установите для него другой массив, при выходе это будет тот новый массив. И даже мутирующие элементы из-за копирования при записи, скорее всего, создадут новый массив, если только параметр не является единственной ссылкой на него. Но почему бы просто не вернуть новый массив и не избежать путаницы?

4. Обновленный ответ, основанный на разъяснении

5. Помните, что массив — это тип значения. Вы ради того, что происходит, можете притвориться, что проходите в Int качестве inout . Если вы назначите Int этому параметру другой, он будет новым Int при возврате из someFunc . Array Dictionary и Set и String ) предназначен для работы таким же образом. Если вы присвоите новое значение всему, что находится внутри someFunc , это будет то, что будет при выходе.