Де я можу знайти функцію “clamp” у .NET?


95

Я хотів би затиснути значення x в діапазоні [a, b]:

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

Це досить елементарно. Але я не бачу функції "затискач" у бібліотеці класів - принаймні, не в System.Math.

(Для неусвідомленого "затискання" значення полягає в тому, щоб переконатися, що воно знаходиться між деякими максимальними та мінімальними значеннями. Якщо воно перевищує максимальне значення, тоді воно замінюється максимальним тощо).


2
@Danvil: Не існує "Бібліотеки класів C #". Ви маєте на увазі ".NET Framework".
Джон Сондерс,

1
Все ще нічого на C # 7.1?
Джос

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/… "Обмеження "було б оманливим, особливо, що" обмеження "вже має інше значення в математиці.
kaalus

Відповіді:


137

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

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 (не впевнений у моно, компактному тощо), загальний слід перекомпілювати для типу, а CompareTo - вбудований, тож не вимагає покарання за продуктивність.
Роберт Фрейзер,

1
@Frasier Якщо це не надмірно чутливий до продуктивності код, навряд чи ти зможеш досягти значного збільшення продуктивності, роблячи це. Мати його загальним, мабуть, корисніше, ніж економити кілька мікросекунд.
MgSam

4
Гарна річ у обмеженні на загальну версію того IComparable, що бокс не відбувається. Це повинно працювати дуже швидко. Пам'ятайте, що за допомогою doubleі float, CompareToметод відповідає загальному порядку, де NaNменше за всі інші значення, включаючи NegativeInfinity. Отже, це не еквівалентно <оператору. Якщо ви використовували <з плаваючою точкою, вам доведеться подумати, як лікувати NaNтакож. Це не актуально для інших числових типів.
Jeppe Stig Nielsen

1
Вам потрібно було б подумати, як лікувати NaNв будь-якому випадку. Версія з <і >буде виводити NaNі використовувати NaNдля minабо maxбуде ефективно зробити один односторонній затиск. З CompareToним завжди повернеться, NaNякщо maxє NaN.
Герман

30

Просто використовуйте Math.Minта Math.Max:

x = Math.Min(Math.Max(x, a), b);

Це означає, 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.
Jim Balter

@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;  
}

6
Тьфу! Ці негарні зайві дужки! Якщо ви збираєтеся бути злим генієм з двосторонніми операторами, принаймні зробіть це правильно і позбудьтесь і цих! 😂
XenoRo

8
@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, а перевернутий порядок аргументів запрошує (і фактично гарантує) помилки. Не знаю, якої двозначності, на вашу думку, уникають.
Jim Balter

Для узгодженості із застарілою реалізацією Math.Clamp рекомендуємо змінити порядок мінімальних / максимальних параметрів:Clamp(T value, T min, T max)
Джош Полі


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;
}

Відмінності:

  • Назва методу використовує відповідний дієслівний час ( ed), щоб (далі) вказувати, що значення не затиснуте на місці, а замість цього повертається нове значення (Див. Коментар @ JimBalter ).
  • Чи підходить null checkдля всіх входів (Див. Коментар @ JeppeStigNielsen ).
  • Свопи minіmax якщо min > max(див коментар @ JeppeStigNielsen в ).

Обмеження: відсутність односторонніх затискачів. Якщо maxє NaN, завжди повертається NaN(Див. Коментар Германа ).


Інше обмеження - nameofне працює для C # 5 або нижче.
RoLYroLL,

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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.