85

鉴于:

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 标准库。

可能没有,如果是这样,那也是我很乐意接受的答案。

4

9 回答 9

131

斯威夫特 4/5

Comparable/Strideable类似于ClosedRange.clamped(to:_) -> ClosedRange标准 Swift 库的扩展。

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
于 2016-11-29T14:48:18.067 回答
59

ClosedInterval类型已经有一个

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

将另一个区间作为参数的方法。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(如Int, CGFloat, String, ...),而且适用于所有类型:

(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
    }
}
于 2016-03-20T07:58:33.907 回答
27

使用与 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)

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

于 2017-10-17T22:04:19.007 回答
15

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)

或任何描述它的东西。

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

于 2020-05-27T13:42:22.840 回答
8

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

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
于 2016-08-05T12:56:28.023 回答
7

Swift 5.1中,实现所需夹紧的惯用方法是使用属性包装器。来自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
于 2019-09-03T18:08:52.553 回答
6

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

extension Comparable {
    func clamped(_ a: Self, _ b: Self) -> Self {
        min(max(self, a), b)
    }
}
于 2020-05-29T09:13:57.187 回答
3

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

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

来源:用户 tobr 在Hacker News 的讨论中

于 2020-08-20T17:40:42.163 回答
0

扩展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
于 2022-02-08T14:37:55.117 回答