#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 проблематичен. Чтобы заставить его работать, вы должны преобразовать его в anInt
, чтобы в основном сохранить действительный адрес в указателе, а затем на принимающей стороне преобразовать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
, это будет то, что будет при выходе.