What's your workflow to recognize whether property getter returns value by reference or by copy

Consider the following example for swift-package manager: swift-package-manager/pbxproj.swift at 03c10bed07709f6b1b2787433ed15001f7ce433c · apple/swift-package-manager · GitHub

How would you check whether modifications applied to targetSettings are reflected in the "origin" (returned by reference) or or not (returned by value):

  • Running some static analysis tool
  • Using debugger
  • Hardcode reading through code

Asking because I often spend too much time trying to understand that when reading through new codebases.

You can tell that it's a reference type because the following line of code (426) modifies a property, even though targetSettings was declared with let.

That's a hint of course, but it implies I have to believe that the code does not have bugs. Assuming I'm not that confident in the codebase, what would you check next?

Strictly speaking, that'd just tell us it's using nonmutating set, but I think that's as far as we can go w/o knowing the original declaration.

2 Likes

In swift everything is is returned by value, never by reference (but the value itself can be a reference to something)
Compare this C++ program that returns something by reference, and swift program that returns a reference by value:
C++: Compiler Explorer
swift: Compiler Explorer

How to check if something is a reference or not? For the first order of approximation, you check if the type of the thing is a class, or a struct. Classes are implemented as references, and structs as values. Things get complicated when you have classes inside structs, or you get indirection in some other way. You cannot know in general, without reading the source code.

2 Likes

In swift everything is is returned by value, never by reference (but the value itself can be a reference to something)

If everything is returned by value, never by reference but some values are references I think it's quite reasonable to say "returns a reference" rather than "returns a value that boxes a reference".

Things get complicated when you have classes inside structs

Which is exactly what you actually encounter in real-life code: many levels of structs embedded in classes embedded in structs. Hence the question.

You cannot know in general, without reading the source code.

The compiler knows, the debugger knows and static analysis tools are at least aware.

And all of those "know" by reading the source code.

My hope was that there is some known workflow to utilize the tools to get a descriptive output before grinding through unfamilier lines of text.

Could you explain why it would matter for reading the code? If you're going to be modifying the code, you should have tools at your disposal to quickly find the declaration.

Surly you would like to understand what the code does :slight_smile:

Could you elaborate and give a couple of examples of such tools that you would use?

I think it's important for everyone to agree what we're even talking about. "what is returned by reference" is a central question for this topic, so we should be precise about that.

Do they? There are many situations where the difference between a reference and value is very blurry, even for humans

  1. Array is a struct that contains a class containing a buffer, but it behaves like a value, not sharing the modifications to that buffer
  2. Are pointers values containing an address, or references to things in memory?
  3. Immutable classes and structs behave exactly the same, is it fair to classify them differently?
  4. this struct is a reference
import Foundation
struct Foo {
    let key: String = String(Int.random(in: 0..<Int.max))
    init(x: Int) { self.x = x }
    var x: Int { 
        get { UserDefaults.standard.integer(forKey: key) }
        nonmutating set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

Oh, and to check if some type is a class or a struct, you either option-click on the type name in Xcode, or hover above it in sourcekit-lsp.

2 Likes

Oh, and to check if some type is a class or a struct, you either option-click on the type name in Xcode,

That's what I currently do, but that's a small improvement.

You're correct and I do not expect "the definite answer" since my question is not precise in the first place. What I do expect is some workflow that would make life easier, that would tell me what is what when it's possible to do so and would give the best possible directions where to proceed when it's not.

I realize that answers are going to be subjective, but that's the nature of my question, I'm interested in learning what other devs are doing in the similar situation.

Your question doesn't have an answer. As others have pointed out, you can know whether the property returns a value type or a reference type, but that gives you absolutely no knowledge about whether the returned value represents a copy of the property's value or not.

For example, if the property's type is a reference type, the property may actually return a copy of the object it stores internally (i.e. a reference to a new object).

Similarly, if the property is a value type, but contains a reference in some interior level, you don't know if the returned value has deep-copied references or not.

Even worse, if the property type uses COW (copy-on-write), the returned value may contain a reference that's not a copy until it (effectively) becomes a copy of some internal object later on.

The only thing you can know for sure is that if the property type is a value type, and the value type recursively contains members of only value types, then the returned value is a genuine copy of the property value. That's one of the reasons that Swift "prefers" value types over reference types.

For everything else, you're on your own. Knowing the mutability semantics of arbitrary values is indeed a general problem, but there's no general solution.

1 Like

I feel a little more optimistic than other replyers. It's a reasonable question, and there are a few possible answers:

  • The first guess is whether a type is a class or not. If it's a class, you know how it behaves. If it's not, it's probably a value.

  • There are some exceptions (structs that have referencey behavior), but usually that's intended to be clear in the name of the type (UnsafePointer, or SwiftUI's Binding), or in the operations you perform on it (Data.write(to:options:) is going to modify the filesystem). If you're not sure, check the documentation for the type (hopefully it has some!).

  • Certain libraries may have specialized reasons to deviate from this rule; for example, UIKit is an Objective-C framework, and so it uses classes where a native Swift library might use structs (for example, UITouch).

In general, anyone can write hard-to-understand code, but most people try not to. Assuming that structs and enums don't have referencey behavior and classes do is a good starting point.

5 Likes