Що можна зробити для поліпшення читабельності математично орієнтованого коду в C #, Java тощо? [зачинено]


16

Як і програміст на C, і програміст на C #, одна з речей, що мені не подобається в C #, - це те, наскільки багатослівні математичні функції. Кожен раз, коли вам доведеться використовувати, наприклад, функцію Sin, косинус або потужність, вам доведеться додавати статичний клас Math. Це призводить до дуже довгого коду, коли саме рівняння досить просте. Проблема стає ще гіршою, якщо вам потрібно ввести типи даних. Як результат, на мою думку, читабельність страждає. Наприклад:

double x =  -Math.Cos(X) * Math.Sin(Z) + Math.Sin(X) * Math.Sin(Y) * Math.Cos(Z);

На відміну від просто

double x = -cos(X) * sin(Z) + sin(X) * sin(Y) * cos(Z);

Це також стосується інших мов, таких як Java.

Я не впевнений, чи справді це питання має рішення, але я хотів би знати, чи є якісь хитрощі, які використовуються програмістами C # або Java для поліпшення читабельності коду Math. Однак я усвідомлюю, що C # / Java / тощо. не є математично орієнтованими мовами на зразок MATLAB або подібними, тому це має сенс. Але іноді потрібно все-таки написати математичний код, і це буде чудово, якщо можна зробити його більш читабельним.


Я конкретно не знаю, але ви, ймовірно, можете знайти бібліотеку алгебри, яка б дозволила вам визначати математичні функції за допомогою рядків, хоча було б певне покарання за продуктивність.
raptortech97


7
Ви турбуєтесь про трохи додаткової багатослівності, але, щасливо, ховаєте "+" серед "*" з одинарними операторами - все без дужок - я підозрюю, що у вас пріоритети неправильні.
mattnz

1
Це був лише приклад, але хороший момент
9a3eedi

6
В C # 6.0, ви зможете написати: using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
svick

Відповіді:


14

Ви можете визначити локальні функції, які викликають глобальні статичні функції. Сподіваємось, компілятор вбудує обертки, і тоді компілятор JIT створить жорсткий код складання для фактичних операцій. Наприклад:

class MathHeavy
{
    private double sin(double x) { return Math.sin(x); }
    private double cos(double x) { return Math.cos(x); }

    public double foo(double x, double y)
    {
        return sin(x) * cos(y) - cos(x) * sin(y);
    }
}

Ви також можете створити функції, які поєднують загальні математичні операції в одні операції. Це дозволило б мінімізувати кількість випадків, коли такі функції подобаються sinта cosз’являються у вашому коді, тим самим роблячи незграбність виклику глобальних статичних функцій менш помітною. Наприклад:

public Point2D rotate2D(double angle, Point2D p)
{
    double x = p.x * Math.cos(angle) - p.y * Math.sin(angle);
    double y = p.x * Math.sin(angle) + p.y * Math.cos(angle);

    return new Point2D(x, y)
}

Ви працюєте на рівні точок і обертів, і основні функції триггера закопуються.


... чому я не подумав про це :)
9a3eedi

Я позначив це правильною відповіддю, тому що це кросплатформенне рішення, яке досить просте. Інші рішення також є правильними. Я дійсно не можу повірити, що я не думав про це, хоча :) це занадто очевидно
9a3eedi

31

В межах Java існує багато інструментів, щоб зробити деякі речі менш багатослівними, ви просто повинні їх знати. Корисним у цьому випадку є staticімпорт ( сторінка підручника) , вікіпедія ).

В цьому випадку,

import static java.lang.Math.*;

class Demo {
    public static void main (String[] args) {
        double X = 42.0;
        double Y = 4.0;
        double Z = PI;

        double x =  -cos(X) * sin(Z) + sin(X) * sin(Y) * cos(Z);
        System.out.println(x);
    }
}

працює досить непогано ( ideone ). Трохи важко вносити статичний імпорт усіх класів Math, але якщо ви багато займаєтеся математикою, то це може бути затребувано.

Статичний імпорт дозволяє імпортувати статичне поле або метод у область імен цього класу та викликати його, не вимагаючи імені пакета. Ви часто це в тих випадках , тест JUnit , де import static org.junit.Assert.*;знаходиться , щоб отримати всі стверджує доступні.


Відмінна відповідь. Мені не було відомо про цю особливість. Під якою версією Java це можливо?
9a3eedi

@ 9a3eedi Вперше він став доступним на Java 1.5.

Гарна техніка. Мені це подобається. +1.
Randall Cook

1
@RandallCook У Java за 1.4 дня люди могли робити такі речі, як public interface Constants { final static public double PI = 3.14; }і тоді, public class Foo implements Constantsу всіх класах, щоб отримати доступ до констант в інтерфейсі. Це зробило великий безлад. Так, з 1,5 додано статичний імпорт, що дозволяє втягувати конкретні константи та статичні функції без необхідності реалізації інтерфейсу.

3
Вибірково можна імпортувати певні функціїimport static java.lang.Math.cos;
храповик, виродка

5

За допомогою C # 6.0 ви можете використовувати функцію статичного імпорту.

Ваш код може бути:

using static System.Math;
using static System.Console;
namespace SomeTestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            double X = 123;
            double Y = 5;
            double Z = 10;
            double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);
            WriteLine(x); //Without System, since it is imported 
        }
    }
}

Див.: Статичне використання операторів (попередній перегляд мови AC № 6.0)

Ще одна особливість «синтаксичного цукру» C # 6.0 - це введення статичного. За допомогою цієї функції можна усунути явне посилання на тип при виклику статичного методу. Крім того, за допомогою статики ви можете вводити лише методи розширення для певного класу, а не всі методи розширення в просторі імен.

EDIT: Оскільки Visual Studio 2015, CTP вийшов у січні 2015 року, для статичного імпорту потрібно чітке ключове слово static. подобається:

using static System.Console;

4

Окрім інших хороших відповідей тут, я також можу порекомендувати DSL для ситуацій зі значною математичною складністю (не середні випадки використання, але, можливо, деякі фінансові чи академічні проекти).

За допомогою інструменту генерації DSL, такого як Xtext , ви можете визначити власну спрощену математичну граматику, яка, в свою чергу, може генерувати клас із вмістом представлених вами формул Java (або будь-якої іншої мови).

Вираз DSL:

domain GameMath {
    formula CalcLinearDistance(double): sqrt((x2 - x1)^2 + (y2 - y1)^2)
}

Створений вихід:

public class GameMath {
    public static double CalcLinearDistance(int x1, int x2, int y1, int y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    }
}

На такому простому прикладі переваги створення граматики та плагіну Eclipse не варто, але для складніших проектів це може принести великі переваги, особливо якщо DSL дасть змогу діловим людям або академічним дослідникам підтримувати офіційні документи в комфортному стані мови, і бути впевненим, що їх робота була точно переведена на мову реалізації проекту.


8
Так, загалом, і за визначенням, DSL може бути корисним під час роботи в певній галузі. Однак якщо цього DSL не існує або він не відповідає потребам, вам доведеться його підтримувати , що може бути проблематично. Крім того, для конкретного запитання ("Як я можу використовувати гріх, cos, ... методи / функції, не пишучи клас Math кожен раз"), DSL - це, можливо, негаразд.
mgoeminne

4

У C # ви можете використовувати методи розширення.

Нижче читається досить красиво, як тільки ви звикли до позначення "postfix":

public static class DoubleMathExtensions
{
    public static double Cos(this double n)
    {
        return Math.Cos(n);
    }

    public static double Sin(this double n)
    {
        return Math.Sin(n);
    }

    ...
}

var x =  -X.Cos() * Z.Sin() + X.Sin() * Y.Sin() * Z.Cos();

На жаль, пріоритет оператора робить речі дещо неприємнішими при роботі з негативними числами. Якщо ви хочете обчислити Math.Cos(-X)замість -Math.Cos(X)вас, вам потрібно буде вкласти число в круглі дужки:

var x = (-X).Cos() ...

1
Між іншим, це зробило б приємний випадок використання властивостей розширення і навіть законний випадок використання зловживань властивостями як методами!
Йорг W Міттаг

Це я думав. x.Sin()зайнялося б певне коригування, але я зловживаю методами розширення, і особисто це було б моїм першим нахилом.
WernerCD

2

C #: Варіація відповіді Рендала Кука , який мені подобається, оскільки він підтримує математичний "вигляд" коду більше, ніж методи розширення, полягає у використанні обгортки, але використанні посилань функцій для викликів, а не загортання їх. Я особисто вважаю, що це робить вигляд коду більш чистим, але в основному він робить те саме.

Я збив трохи тестову програму LINQPad, включаючи обернені функції Рандалла, посилання на мої функції та прямі дзвінки.

Виклики, на які посилаються функції, в основному займають той же час, що і прямі дзвінки. Функції, які завершуються, послідовно повільніші - хоча і не на величезну кількість.

Ось код:

void Main()
{
    MyMathyClass mmc = new MyMathyClass();

    System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

    for(int i = 0; i < 50000000; i++)
        mmc.DoStuff(1, 2, 3);

    "Function reference:".Dump();
    sw.Elapsed.Dump();      
    sw.Restart();

    for(int i = 0; i < 50000000; i++)
        mmc.DoStuffWrapped(1, 2, 3);

    "Wrapped function:".Dump();
    sw.Elapsed.Dump();      
    sw.Restart();

    "Direct call:".Dump();
    for(int i = 0; i < 50000000; i++)
        mmc.DoStuffControl(1, 2, 3);

    sw.Elapsed.Dump();
}

public class MyMathyClass
{
    // References
    public Func<double, double> sin;
    public Func<double, double> cos;
    public Func<double, double> tan;
    // ...

    public MyMathyClass()
    {
        sin = System.Math.Sin;
        cos = System.Math.Cos;
        tan = System.Math.Tan;
        // ...
    }

    // Wrapped functions
    public double wsin(double x) { return Math.Sin(x); }
    public double wcos(double x) { return Math.Cos(x); }
    public double wtan(double x) { return Math.Tan(x); }

    // Calculation functions
    public double DoStuff(double x, double y, double z)
    {
        return sin(x) + cos(y) + tan(z);
    }

    public double DoStuffWrapped(double x, double y, double z)
    {
        return wsin(x) + wcos(y) + wtan(z);
    }

    public double DoStuffControl(double x, double y, double z)
    {
        return Math.Sin(x) + Math.Cos(y) + Math.Tan(z);
    }
}

Результати:

Function reference:
00:00:06.5952113

Wrapped function:
00:00:07.2570828

Direct call:
00:00:06.6396096

1

Використовуйте Scala! Ви можете визначити символічні оператори, і вам не потрібні паролі для ваших методів. Це робить математику так легше інтерпретувати.

Наприклад, той же розрахунок у Scala та Java може бути чимось на кшталт:

// Scala
def angle(u: Vec, v: Vec) = (u*v) / sqrt((u*u)*(v*v))

// Java
public double angle(u: Vec, v: Vec) {
  return u.dot(v) / sqrt(u.dot(u)*v.dot(v));
}

Це додається досить швидко.


2
Scala недоступна в CLR, лише JVM. Таким чином, це насправді не є життєздатною альтернативою C #.
ben rudgers

@benrudgers - C # не працює на JVM, тому це насправді не є життєздатною альтернативою Java, про яку також було задано питання. Питання не вказує, що це повинен бути CLR!
Рекс Керр

Можливо, я люддит, але два зайвих символи для "крапки" замість "*", з тим перевагою, що код чіткіший, здається, невелику ціну. Все-таки хороша відповідь.
user949300
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.