Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 3 silently allows shadowing a parameter

Tags:

swift

I'm switching to Swift, and I'm really not happy that the following code compiles without a warning:

func f(_ x: inout Int?) {
    var x: Int? // <-- this declaration should produce a warning
    x = 105
    if x! < 1000 {}
}

var a: Int? = 3
f(&a)
print("\(a)")

and, of course, outputs Optional(3) upon execution.

In this example, the x local variable shadows the x function parameter.

Turning on the Hidden Local Variables warning (GCC_WARN_SHADOW) in the project settings doesn't cause a warning to be produced either.

Question: How should I proceed to make the Swift 3 compiler warn me about such shadowing?

like image 413
Sea Coast of Tibet Avatar asked Oct 11 '16 17:10

Sea Coast of Tibet


People also ask

How do you avoid variable shadowing?

Variable shadowing could be avoided by simply renaming variables with unambiguous names. We could rewrite the previous examples: The inner scope has access to variables defined in the outer scope.

What is variable shadowing in Swift?

In Swift, shadowing only occurs where the user deliberately chooses a new variable in a local scope with the exact name as a variable in the outer scope. Put another way, it is precisely the repetition of an identical name that is the shadowing.

Why is variable shadowing bad?

Shadowing of local variables should generally be avoided, as it can lead to inadvertent errors where the wrong variable is used or modified. Some compilers will issue a warning when a variable is shadowed.

What is shadowing a parameter?

Declaring a variable with a name that already refers to another variable is called shadowing. In this case, you shadow a function argument.


1 Answers

While you might have found useful solutions already, Apple's Documentation on Functions actually has a comment on this exact type of use. You requested an answer for why code highlighting is not warning you of the naming conflict, but the main reason why you are probably not getting any warning is because inout parameters, and all parameters, do not take precedence over the variables initialized within the function (they are only copies of the value they represent when being manipulated inside of the function). So your function, as i will illustrate bellow, does not consider the paramater your are passing in, because you initialize a new variable with the same name. Therefore, by the rules that parameters are governed, your passed parameter is completely ignored. I see your frustrastion, as in some other languages this would be a compiler error. However, with inouts here, this is simply not the case as a convention. See here in the docs :

In-out parameters are passed as follows:

When the function is called, the value of the argument is copied. In the body of the function, the copy is modified. When the function returns, the copy’s value is assigned to the original argument. This behavior is known as copy-in copy-out or call by value result. For example, when a computed property or a property with observers is passed as an in-out parameter, its getter is called as part of the function call and its setter is called as part of the function return.

As an optimization, when the argument is a value stored at a physical address in memory, the same memory location is used both inside and outside the function body. The optimized behavior is known as call by reference; it satisfies all of the requirements of the copy-in copy-out model while removing the overhead of copying. Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.

Do not access the value that was passed as an in-out argument, even if the original argument is available in the current scope. When the function returns, your changes to the original are overwritten with the value of the copy. Do not depend on the implementation of the call-by-reference optimization to try to keep the changes from being overwritten. [..]

In your case if you were to actually modify the parameter you were passing, you'd use something similar to this:

If you need to capture and mutate an in-out parameter, use an explicit local copy, such as in multithreaded code that ensures all mutation has finished before the function returns.

func multithreadedFunction(queue: DispatchQueue, x: inout Int) {
    // Make a local copy and manually copy it back.
    var localX = x
    defer { x = localX }

    // Operate on localX asynchronously, then wait before returning.
    queue.async { someMutatingOperation(&localX) }
    queue.sync {}
}

So as you can see here, while localX is not called x, like what you do, localX takes on a whole other instance of memory to contain data. Which in this case is the same value as x, but is not the instance of x so it does not compile as a naming error. To show that this still applies when you change localX to var x = Int? like you do inside of your function:

func f(_ x: inout Int?) {
    print(x, "is x")
    var x: Int? // <-- this declaration should produce a warning
    print(x, "is x after initializing var x : Int?")
    x = 105
    print(x, "is x after giving a value of 105")
    if x! < 1000 {}
}

var a: Int? = 3
f(&a)
print("\(a)", "is x after your function")

Returns:

Optional(3) is x
nil is x after initializing var x: Int?
Optional(105) is x after giving a value of 105 to x
Optional(3) is x after your function

To show you just how far this goes, i'll use what mohsen did to show you that he was not completely wrong in his logic to show you this rule in the convention, while i agree that he did not address the lack of code warning in your question.

func f(_ x: inout Int?) {
    print(x, "is inout x")
    var y: Int? // <-- this declaration should produce a warning
    print(x, "is inout x and ", y, "is y")
    x = 105
    print(x, "is inout x and ", y, "is y after giving a value of 105 to inout x")
    if x! < 1000 {}
}

var a: Int? = 3
f(&a)
print("\(a)", "is x after your function")

Prints:

Optional(3) is inout x
Optional(3) is inout x and  nil is y
Optional(105) is inout x and  nil is y after giving a value of 105 to inout x
Optional(105) is x after your function

So as you can see here in the first function, your inout parameters and paramaters in general, cease to take precedence over what is contained inside, because it technically has no initializtion inside of the function, which is the purpose of the inout convention itself: the function saves that value in memory, assigns that memory instance a pointer, and whatever mutations are applied to that pointer are then applied to the original variable that is outside of the scope of the function when the function ends. So whatever mutations you might have done to it after var x: Int? wont alter the variable in your inout parameter when return is hit, because you've overridden the pointer assigned to the letter x. To show you that this is not the case with non-inouts, we'll assign a distinct variable from x:

func f(_ x: Int?) {
    print(x!, "is inout x")
    var y: Int? // <-- this declaration should produce a warning
    print(x!, "is inout x and ", y!, "is y")
    x = 105
    y = 100
    print(x!, "is inout x and ", y!, "is y after giving a value of 105 to inout x")
    if x! < 1000 {}
}

var a: Int? = 3
f(a)
print("\(a!)", "is x after your function")

Returns

Playground execution failed: error: SomeTest.playground:6:7: error: cannot assign to value: 'x' is a 'let' constant
    x = 105

But, if i go back to the original function and rename the new variable to the same pointer as the name of the parameter:

func f(_ x: Int?) {
    print(x, "is inout x")
    var x: Int? // <-- this declaration should produce a warning
    print(x, "is inout x and ")
    x = 100
    print(x, "is inout x and ")
    if x! < 1000 {}
}

var a: Int? = 3
f(a)
print("\(a!)", "is x after your function")

we get:

Optional(3) is inout x
nil is inout x and 
Optional(100) is inout x and 
3 is x after your function

So all in all, the inout parameter and the standard paramater is never modified, because, within the scope of the function, the pointer for x is completely overridden with Int?.

This is why you don't get a code warning, and technically, you shouldn't because of the conventions surrounding parameters dictate that what you wrote is not a compiling conflict and is valid code (perhaps it might not be for your use case, but conventionally, it is) and so you most likely will not be able to find a way to highlight this naming issue.

like image 91
jlmurph Avatar answered Oct 20 '22 18:10

jlmurph