【问题标题】:Standard way to "clamp" a number between two values in Swift在 Swift 中“钳位”两个值之间的数字的标准方法
【发布时间】:2016-03-20 05:41:11
【问题描述】:

给定:

let a = 4.2
let b = -1.3
let c = 6.4

我想知道将这些值限制在给定范围内的最简单、最快捷的方法,例如 0...5,这样:

a -> 4.2
b -> 0
c -> 5

我知道我可以做到以下几点:

let clamped = min(max(a, 0), 5)

或者类似的东西:

let clamped = (a < 0) ? 0 : ((a > 5) ? 5 : a)

但我想知道在 Swift 中是否还有其他方法可以做到这一点——特别是,我想知道(并在 SO 上记录,因为在 Swift 中似乎没有关于钳位数字的问题)是否存在是 Swift 标准库中专门用于此目的的任何内容。

可能没有,如果有,我也很乐意接受这个答案。

【问题讨论】:

    标签: swift clamp


    【解决方案1】:

    斯威夫特 4/5

    Comparable/Strideable 的扩展类似于标准 Swift 库中的ClosedRange.clamped(to:_) -&gt; ClosedRange

    extension Comparable {
        func clamped(to limits: ClosedRange<Self>) -> Self {
            return min(max(self, limits.lowerBound), limits.upperBound)
        }
    }
    
    #if swift(<5.1)
    extension Strideable where Stride: SignedInteger {
        func clamped(to limits: CountableClosedRange<Self>) -> Self {
            return min(max(self, limits.lowerBound), limits.upperBound)
        }
    }
    #endif
    

    用法:

    15.clamped(to: 0...10) // returns 10
    3.0.clamped(to: 0.0...10.0) // returns 3.0
    "a".clamped(to: "g"..."y") // returns "g"
    
    // this also works (thanks to Strideable extension)
    let range: CountableClosedRange<Int> = 0...10
    15.clamped(to: range) // returns 10
    

    【讨论】:

    • 您的示例仅适用于 Comparable 扩展 - Strideable 扩展在这里是多余的,除非有另一个未演示的用例?
    • Strideable 扩展用于钳制到 CountableClosedRange。如果您使用范围文字编译器可以创建正确的范围类型,但如果您使用变量作为参数,则需要 Strideable 扩展。我将扩展示例。
    • @OndrejStocek 似乎 Swift 5.1 不再需要 Strideable 扩展。
    • @Fattie 这就是这个扩展的重点——将最小/最大使用量降至最低。在所有其他情况下,您可以使用 .clamped(to: ...) 而不是 min(max(...)...)
    • @Fattie 那么正确的做法是什么?实际上,由于... 封闭范围运算符,您确实知道范围包括边界。
    【解决方案2】:

    ClosedInterval 类型已经有一个

    func clamp(_ intervalToClamp: ClosedInterval<Bound>) -> ClosedInterval<Bound>
    

    将另一个 interval 作为参数的方法。有一个 关于 Swift Evolution 邮件列表的提案

    添加另一种将单个值限制在给定区间的方法:

    /// Returns `value` clamped to `self`.
    func clamp(value: Bound) -> Bound
    

    这正是你所需要的。

    使用现有clamp()方法的实现

    例如,这个额外的clamp()方法可以实现为

    extension ClosedInterval {
        func clamp(value : Bound) -> Bound {
            return self.start > value ? self.start
                : self.end < value ? self.end
                : value
        }
    }
    

    例子:

    (0.0 ... 5.0).clamp(4.2)    // 4.2
    (0.0 ... 5.0).clamp(-1.3)   // 0.0
    (0.0 ... 5.0).clamp(6.4)    // 5.0
    

    ClosedInterval 是一个通用类型

    public struct ClosedInterval<Bound : Comparable> { ... }
    

    因此,这不仅适用于Double,而且适用于所有人 Comparable 的类型(如 IntCGFloatString、...):

    (1 ... 3).clamp(10)      // 3
    ("a" ... "z").clamp("ä") // "ä"
    

    Swift 3 (Xcode 8) 更新: ClosedInterval 已重命名 到ClosedRange,它的属性现在是lower/upperBound

    extension ClosedRange {
        func clamp(_ value : Bound) -> Bound {
            return self.lowerBound > value ? self.lowerBound
                : self.upperBound < value ? self.upperBound
                : value
        }
    }
    

    【讨论】:

    • 太棒了!这正是我正在寻找的信息。希望在 Swift 的未来版本中实现这一点。
    • 作为土生土长的瑞典人,(字母:..., x, y, z, å, ä, ö),"ä" &lt; "z" // true 有点令人困惑;)另一方面,列出的clamp 示例需要与外部参数名称@ 一起使用987654346@ 用于 Swift 3 示例,可能值得明确提及(或者 Xcode 将修复该提示!)。
    • @dfri:字符串和字符比较区域设置敏感,比较stackoverflow.com/questions/25713975/…。添加了_ 外部参数名称以使示例再次工作。感谢您的反馈!
    【解决方案3】:

    使用与 Apple 相同的语法来做 min 和 max 运算符:

    public func clamp<T>(_ value: T, minValue: T, maxValue: T) -> T where T : Comparable {
        return min(max(value, minValue), maxValue)
    }
    

    你可以这样使用:

    let clamped = clamp(newValue, minValue: 0, maxValue: 1)
    

    这种方法很酷的一点是,任何值都定义了执行操作所需的类型,因此编译器会自行处理。

    【讨论】:

      【解决方案4】:

      2020 年。极其简单的方法。

      extension Comparable {
          func clamped(_ f: Self, _ t: Self)  ->  Self {
              var r = self
              if r < f { r = f }
              if r > t { r = t }
              // (use SIMPLE, EXPLICIT code here to make it utterly clear
              // whether we are inclusive, what form of equality, etc etc)
              return r
          }
      

      虽然我真的很喜欢 Swift 中的范围,但我真的认为钳制函数的绝对标准语法(“50 年来在每种计算机语言中”)只是更简单更好:

      x = x.clamped(0.5, 5.0)
      

      在它内置到 Swift 之前,我真的认为这是最好的。

      哲学角:

      IMO 钳位函数中的两个值并不是真正的“范围” - 它们只是“两个值”。

      (例如:在游戏代码中,两个动态值有时处于“错误的顺序”(即,期望的结果是外部的)或相同(结果就是这样)是完全常见的值)。)

      关于结尾命名的意见...

      在我们所做的每一件事上,我们都坚持明确说明是否具有包容性或排他性。例如,如果有电话

      randomIntUpTo( 13 )
      

      其实我们会命名的

      randomIntUpToExclusive( 13 )
      

      如果是这样的话,或者确实是“包容性的”。或者取决于语言类似

      randomInt(fromInclusive:  upToExclusive: )
      

      或任何情况。这样一来,绝对永远不会出现统一错误,无需讨论任何内容。所有代码名称都应该是自记录的。所以确实,对我们来说,上面的函数会被命名为

      func clamped(fromExclusive: Self, toExclusive: Self)
      

      或任何描述它的东西。

      但这只是我们。但这是正确的做法:)

      【讨论】:

      • 您对不一定与钳位相关的范围以及两个钳位值的顺序提出了很好的看法。您的实施的较短版本可能是:func clamped(_ a: Self, _ b: Self) -&gt; Self { max(min(self, a), b) }
      • 这并不简单,因为你创建了额外的变量但你可以直接返回值
      • 嗨@VyachaslavGerchicov - 这就是重点!用最简单、最清晰、最明确的方式来做。 不是最短或最简洁的方式
      • 这个答案没有利用 2020 年添加的任何新内容。也对现有答案没有贡献。
      【解决方案5】:

      在 Swift 3 中有新的 CountableClosedRangeCountableRangeRangeClosedRange 协议。它们具有相同的 upperBoundlowerBound 属性。因此,您可以通过声明自定义协议,使用clamp 方法一次性扩展所有Range 协议:

      protocol ClampableRange {
      
          associatedtype Bound : Comparable
      
          var upperBound: Bound { get }
      
          var lowerBound: Bound { get }
      
      }
      
      extension ClampableRange {
      
          func clamp(_ value: Bound) -> Bound {
              return min(max(lowerBound, value), upperBound)
          }  
      
      }
      
      extension Range : ClampableRange {}
      extension ClosedRange : ClampableRange {}
      extension CountableRange : ClampableRange {}
      extension CountableClosedRange : ClampableRange {}
      

      用法:

      (0...10).clamp(12) // 10
      (0..<100).clamp(-2) // 0
      ("a"..."c").clamp("z") // c
      

      【讨论】:

      • (0..&lt;100).clamp(123) 返回100,这可能出乎意料,因为该数字不包含在区间中。
      • 所有示例实现都存在上述问题!
      • 这是仅对封闭范围实施钳位的好理由。
      【解决方案6】:

      使用 Swift 5.1,实现所需钳位的惯用方法是使用 property wrappers。来自NSHipster 的修饰示例:

      @propertyWrapper
      struct Clamping<Value: Comparable> {
        var value: Value
        let range: ClosedRange<Value>
      
        init(wrappedValue: Value, _ range: ClosedRange<Value>) {
          precondition(range.contains(wrappedValue))
          self.value = wrappedValue
          self.range = range
        }
      
        var wrappedValue: Value {
          get { value }
          set { value = min(max(range.lowerBound, newValue), range.upperBound) }
        }
      }
      

      用法:

      @Clamping(0...5) var a: Float = 4.2
      @Clamping(0...5) var b: Float = -1.3
      @Clamping(0...5) var c: Float = 6.4
      

      【讨论】:

      • 这对我来说似乎非常冗长和不直观! let x = 1.42.clamped(to: 0...1)let x = clamp(1.42, to: 0...1)@Clamping(0...1) let x = 1.42 干净。
      • 非常有趣的解决方案。谢谢。
      【解决方案7】:

      跟进@Fattie 的回答和我的评论,为了清楚起见,这是我的建议:

      extension Comparable {
          func clamped(_ a: Self, _ b: Self) -> Self {
              min(max(self, a), b)
          }
      }
      

      【讨论】:

      • 要明确的是,我个人不建议这样做(如我的回答/cmets 中所述)
      【解决方案8】:

      最短(但可能不是最有效)的夹紧方式是:

      let clamped = [0, a, 5].sorted()[1]
      

      来源:discussion on Hacker News中的用户tobr

      【讨论】:

        【解决方案9】:

        扩展 FixedWidthInteger 并创建一个实例泛型方法来接受 RangeExpression 并处理边缘情况:

        extension FixedWidthInteger {
            func clamped<R: RangeExpression>(with range: R) -> Self where R.Bound == Self {
                switch range {
                case let range as ClosedRange<Self>:
                    return Swift.min(range.upperBound, Swift.max(range.lowerBound, self))
                case let range as PartialRangeFrom<Self>:
                    return Swift.max(range.lowerBound, self)
                case let range as PartialRangeThrough<Self>:
                    return Swift.min(range.upperBound, self)
                case let range as Range<Self>:
                    return Swift.min(range.dropLast().upperBound, Swift.max(range.lowerBound, self))
                case let range as PartialRangeUpTo<Self>:
                    return Swift.min(range.upperBound.advanced(by: -1), self)
                default: return self
                }
            }
        }
        

        游乐场测试:

        100.clamped(with: 1...)     // 100
        100.clamped(with: ..<100)   // 99
        100.clamped(with: ...100)   // 100
        100.clamped(with: 1..<100)  // 99
        100.clamped(with: 1...100)  // 100
        
        0.clamped(with: 1...)       // 1
        0.clamped(with: ..<100)     // 0
        0.clamped(with: ...100)     // 0
        0.clamped(with: 1..<100)    // 1
        0.clamped(with: 1...100)    // 1
        

        要通过 FloatingPoint 实现获得相同的结果,您可以将其 nextDown 属性用于边缘情况:

        extension BinaryFloatingPoint {
            func clamped<R: RangeExpression>(with range: R) -> Self where R.Bound == Self {
                switch range {
                case let range as ClosedRange<Self>:
                    return Swift.min(range.upperBound, Swift.max(range.lowerBound, self))
                case let range as PartialRangeFrom<Self>:
                    return Swift.max(range.lowerBound, self)
                case let range as PartialRangeThrough<Self>:
                    return Swift.min(range.upperBound, self)
                case let range as Range<Self>:
                    return Swift.min(range.upperBound.nextDown, Swift.max(range.lowerBound, self))
                case let range as PartialRangeUpTo<Self>:
                    return Swift.min(range.upperBound.nextDown, self)
                default: return self
                }
            }
        }
        

        游乐场测试:

        let value = 100.0
        
        value.clamped(with: 1...)     // 100
        value.clamped(with: ..<100)   // 99.99999999999999
        value.clamped(with: ...100)   // 100
        value.clamped(with: 1..<100)  // 99.99999999999999
        value.clamped(with: 1...100)  // 100
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-04-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-01-15
          • 2015-09-13
          • 2015-02-04
          • 1970-01-01
          相关资源
          最近更新 更多