Где я могу найти в .NET функцию «зажим»?

98

Я хотел бы зафиксировать значение xв диапазоне [a, b]:

x = (x < a) ? a : ((x > b) ? b : x);

Это довольно просто. Но я не вижу в библиотеке классов функции "зажим" - по крайней мере, не в System.Math.

(Для неосведомленных «зажимать» значение - это убедиться, что оно находится между некоторыми максимальными и минимальными значениями. Если оно больше максимального значения, оно заменяется максимальным и т. Д.)

Danvil
источник
2
@Danvil: не существует "библиотеки классов C #". Вы имеете в виду «.NET Framework».
Джон Сондерс
1
По-прежнему ничего по сравнению с C # 7.1?
joce
1
@JohnSaunders Я не верю, что это строго правда stackoverflow.com/questions/807880/…
Адам Нейлор
Если бы я спросил, как «ограничить» значение, каждый англоговорящий программист в мире сразу понял бы, что я имел в виду. Скорее всего, это знает каждый программист. После 30 с лишним лет в бизнесе мне пришлось выяснить, что сегодня означает «зажим». Подобно «внедрению зависимостей» - «параметризация» - такая очевидная вещь, о которой никто никогда не писал книги.
Боб
@Bob Некоторые слова имеют историческое, четко определенное значение. Зажим - один из них. en.wikipedia.org/wiki/Clamping_(graphics) или khronos.org/registry/OpenGL-Refpages/gl4/html/clamp.xhtml или docs.microsoft.com/en-us/windows/win32/direct3dhlsl/… "Ограничение «вводит в заблуждение, особенно то, что« предел »уже имеет другое значение в математике.
Каалус

Ответы:

141

Вы можете написать метод расширения:

public static T Clamp<T>(this T val, T min, T max) where T : IComparable<T>
{
    if (val.CompareTo(min) < 0) return min;
    else if(val.CompareTo(max) > 0) return max;
    else return val;
}

Методы расширения относятся к статическим классам - поскольку это довольно низкоуровневая функция, она, вероятно, должна находиться в каком-то основном пространстве имен вашего проекта. Затем вы можете использовать этот метод в любом файле кода, который содержит директиву using для пространства имен, например

using Core.ExtensionMethods

int i = 4.Clamp(1, 3);

.NET Core 2.0

Начиная с .NET Core 2.0 System.Mathтеперь есть Clampметод, который можно использовать вместо этого:

using System;

int i = Math.Clamp(4, 1, 3);
Ли
источник
1
Куда бы я поместил это и вызывает CompareTo медленнее, чем сравнение с <(для целочисленных типов)?
Danvil
1
В статическом классе и в .NET framework (не уверен в моно, компактности и т. Д.) Общий тип должен быть перекомпилирован для типа, а CompareTo встроен, чтобы не снижать производительность.
Роберт Фрейзер
1
@Frasier Если это не особо чувствительный к производительности код, вы вряд ли добьетесь значительного увеличения производительности, сделав это. Иметь общий вид, вероятно, полезнее, чем сэкономить несколько микросекунд.
MgSam
5
Преимущество ограничения общей версией IComparableзаключается в том, что не происходит упаковки. Это должно работать очень быстро. Помните, что с doubleи float, CompareToметод соответствует общему порядку, где NaNменьше всех остальных значений, включая NegativeInfinity. Так что это не эквивалент <оператора. Если вы использовали <тип с плавающей запятой, вам также следует подумать, как лечить NaN. Это не актуально для других числовых типов.
Йеппе Стиг Нильсен
1
Вам нужно будет подумать, как лечить NaNв любом случае. Версия с <и >могла бы выводить NaNи использовать NaNдля minили maxэффективно создавала бы односторонний зажим. С CompareToним всегда вернется, NaNесли maxесть NaN.
Herman
32

Просто используйте Math.Minи Math.Max:

x = Math.Min(Math.Max(x, a), b);
d7самурай
источник
Это означает, int a0 = x > a ? x : a; return a0 < b ? a0 : bчто (хотя и дает правильные результаты) не совсем идеально.
Мистер Смит
12
и почему так?
d7samurai
4
@ d7samurai Если мы знаем, что min <= max, Math.Min(Math.Max(x, min), max)приведет к еще одному сравнению, чем необходимо, если x <min.
Джим Балтер
@JimBalter, теоретически это правда. Если вы посмотрите, как обычно реализуется CompareTo (), принятый ответ может потребовать до 6 сравнений. Однако я не знаю, достаточно ли умен компилятор, встроил ли CompareTo () и удалял ли лишние сравнения.
quinmars
1
Это хорошо для случаев, когда вам нужно сделать это только один раз, и тогда совершенно новая функция для этого кажется излишней.
feos
26

Пытаться:

public static int Clamp(int value, int min, int max)  
{  
    return (value < min) ? min : (value > max) ? max : value;  
}
Клитор
источник
7
Ух! Эти уродливые повторяющиеся скобки! Если вы собираетесь стать злым гением с двойными тернарными операторами, по крайней мере, сделайте это правильно и избавьтесь от них! 😂
XenoRo 01
9
@XenoRo Эти «лишние» скобки делают его читабельным.
Четче
2
@Cleaner - 1) Если вы стремитесь к удобочитаемости, можно избежать двойных троек и вместо них использовать блоки IF. 2) Вы не понимаете анекдота? xD
XenoRo
13

Его нет, но сделать его несложно. Я нашел здесь: зажим

Это:

public static T Clamp<T>(T value, T max, T min)
    where T : System.IComparable<T> {
        T result = value;
        if (value.CompareTo(max) > 0)
            result = max;
        if (value.CompareTo(min) < 0)
            result = min;
        return result;
    }

И его можно использовать как:

int i = Clamp(12, 10, 0); -> i == 10
double d = Clamp(4.5, 10.0, 0.0); -> d == 4.5
Джереми Б.
источник
Это решение лучше принятого. Никакой двусмысленности.
aggsol
6
@CodeClown Это решение приводит к ненужному сравнению, когда значение> max, а инвертированный порядок аргументов вызывает (и фактически гарантирует) ошибки. Я не знаю, какой двусмысленности, по вашему мнению, можно избежать.
Джим Балтер
Для согласованности с устаревшей реализацией Math.Clamp рекомендуется переключить порядок параметров min / max:Clamp(T value, T min, T max)
josh poley
4

Просто поделитесь решением Ли с рассмотренными проблемами и проблемами в комментариях, где это возможно:

public static T Clamped<T>(this T value, T min, T max) where T : IComparable<T> {
    if (value == null) throw new ArgumentNullException(nameof(value), "is null.");
    if (min == null) throw new ArgumentNullException(nameof(min), "is null.");
    if (max == null) throw new ArgumentNullException(nameof(max), "is null.");
    //If min <= max, clamp
    if (min.CompareTo(max) <= 0) return value.CompareTo(min) < 0 ? min : value.CompareTo(max) > 0 ? max : value;
    //If min > max, clamp on swapped min and max
    return value.CompareTo(max) < 0 ? max : value.CompareTo(min) > 0 ? min : value;
}

Отличия:

Ограничения: Нет односторонних зажимов. Если maxесть NaN, всегда возвращается NaN(см . Комментарий Германа ).

XenoRo
источник
Другое ограничение - nameofне работает для C # 5 или ниже.
RoLYroLLs
0

Используя предыдущие ответы, я сократил его до приведенного ниже кода для своих нужд. Это также позволит вам ограничивать число только его минимальным или максимальным значением.

public static class IComparableExtensions
{
    public static T Clamped<T>(this T value, T min, T max) 
        where T : IComparable<T>
    {
        return value.CompareTo(min) < 0 ? min : value.ClampedMaximum(max);
    }

    public static T ClampedMinimum<T>(this T value, T min)
        where T : IComparable<T>
    {
        return value.CompareTo(min) < 0 ? min : value;
    }

    public static T ClampedMaximum<T>(this T value, T max)
        where T : IComparable<T>
    {
        return value.CompareTo(max) > 0 ? max : value;
    }
}
Бобби Спирс
источник
А почему бы и нет return value.ClampedMinimum(min).ClampedMaximum(max);?
Хенрик
0

Приведенный ниже код поддерживает указание границ в любом порядке (например bound1 <= bound2, или bound2 <= bound1). Я нашел это полезным для значений зажима, рассчитанных по линейным уравнениям ( y=mx+b), где наклон линии может увеличиваться или уменьшаться.

Я знаю: код состоит из пяти супер-уродливых операторов условных выражений . Дело в том, что он работает , и приведенные ниже тесты это подтверждают. Не стесняйтесь добавлять совершенно ненужные скобки, если хотите.

Вы можете легко создавать другие перегрузки для других числовых типов и в основном копировать / вставлять тесты.

Предупреждение: сравнивать числа с плавающей запятой непросто. Этот код не обеспечивает doubleнадежных сравнений. Используйте библиотеку сравнения с плавающей запятой, чтобы заменить использование операторов сравнения.

public static class MathExtensions
{
    public static double Clamp(this double value, double bound1, double bound2)
    {
        return bound1 <= bound2 ? value <= bound1 ? bound1 : value >= bound2 ? bound2 : value : value <= bound2 ? bound2 : value >= bound1 ? bound1 : value;
    }
}

xUnit / FluentAssertions тесты:

public class MathExtensionsTests
{
    [Theory]
    [InlineData(0, 0, 0, 0)]
    [InlineData(0, 0, 2, 0)]
    [InlineData(-1, 0, 2, 0)]
    [InlineData(1, 0, 2, 1)]
    [InlineData(2, 0, 2, 2)]
    [InlineData(3, 0, 2, 2)]
    [InlineData(0, 2, 0, 0)]
    [InlineData(-1, 2, 0, 0)]
    [InlineData(1, 2, 0, 1)]
    [InlineData(2, 2, 0, 2)]
    [InlineData(3, 2, 0, 2)]
    public void MustClamp(double value, double bound1, double bound2, double expectedValue)
    {
        value.Clamp(bound1, bound2).Should().Be(expectedValue);
    }
}
НатанАлден-старший
источник
0

Если я хочу проверить диапазон аргумента в [min, max], я использую следующий удобный класс:

public class RangeLimit<T> where T : IComparable<T>
{
    public T Min { get; }
    public T Max { get; }
    public RangeLimit(T min, T max)
    {
        if (min.CompareTo(max) > 0)
            throw new InvalidOperationException("invalid range");
        Min = min;
        Max = max;
    }

    public void Validate(T param)
    {
        if (param.CompareTo(Min) < 0 || param.CompareTo(Max) > 0)
            throw new InvalidOperationException("invalid argument");
    }

    public T Clamp(T param) => param.CompareTo(Min) < 0 ? Min : param.CompareTo(Max) > 0 ? Max : param;
}

Класс работает для всего объекта, который есть IComparable. Создаю экземпляр с определенным диапазоном:

RangeLimit<int> range = new RangeLimit<int>(0, 100);

Я либо подтверждаю аргумент

range.Validate(value);

или зажмите аргумент до диапазона:

var v = range.Validate(value);
Кролик76
источник