Почему в MQ записывают методы класса раздельно?

 

Хотел кое-что выложить в codebase и возникли вопросы. Я записываю классы в MQL способом, показанным ниже. Кстати, запустил стилизатор, чтобы привести скобки к стилю K&R, он выдал какие-то гиганские отступы, так что оставлю свой стиль от MS, это несуществено. Итак вопросы:

  1. Можно ли выкладывать классы в таком виде, как ниже? То есть нет разделения на объявление метода и его реализацию, реализация пишется в теле класса.
  2. В чем смысл того, что MQ используют такое разделение?

И мои соображения. Исторически такое разделение пошло в Си, там, как вы знаете, объявления выносятся в отдельный .h файл. Вызвано было в первую очередь тем, что на слабых компах того времени делали раздельную компиляцию модулей, которые хранились на магнитных лентах. Процесс, понятно, не быстрый. И уже потом линкер собирал из .obj - файлов конечный код. То есть при изменении одного модуля не надо было 3 часа перекомпилировать весь проект, достаточно было только сделать сборку. И для этого линкеру нужны были .h файлы с объявлениями.

Второе, при передаче кода на сторону можно было передать скомпилированные .obj и .lib файлы, закрывая тем самым свои исходники.

Раздельную компиляцию MQL не поддерживает, нет даже возможности создать проект с несколькими .mq4/5 файлами. Выносить в отдельный заголовочник определения методов класса тоже вроде нельзя (или можно?). В общем было бы интересно услышать мотивацию от представителей MQ.

P.S Кстати, как вы знаете, в современных языках типа C# вообще отказались от заголовочных файлов ввиду ненужности и код пишется в нижеприведенном стиле.

#property strict
#include <Object.mqh>

class CLogFile: public CObject
{
protected:
    int m_logFile;
public:
    CLogFile() // конструктор без параметров
    {
        string logFileName;
        if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE) || MQLInfoInteger(MQL_OPTIMIZATION))
        {
            logFileName = MQLInfoString(MQL_PROGRAM_NAME) + "_tester.log";
            m_logFile = FileOpen(logFileName, FILE_CSV | FILE_WRITE, ','); // нет нужды при тестировании сохранять старые данные, затираем при открытии файла
        }
        else
        {
            logFileName = MQLInfoString(MQL_PROGRAM_NAME) + ".log";
            m_logFile = FileOpen(logFileName, FILE_CSV | FILE_READ | FILE_WRITE | FILE_SHARE_READ, ',');
        }
    };
    CLogFile(int logfile){ m_logFile = logfile; }

    ~CLogFile(){ FileClose(m_logFile);}

    void WriteMsg(string msg)
    {
        if(m_logFile > 0) 
        {
            string dts = TimeToStr(TimeCurrent(), TIME_DATE | TIME_MINUTES | TIME_SECONDS);
            FileWrite(m_logFile, dts, "  ", msg);
        }
    }
};
 

Пишу, как и Вы, - исходники методов внутри классов. Но бывают ситуации, когда это "неудобно".

Например, у вас определен класс ЧАСТИЧНО (не все методы имеют исходники) в mqh-файле. И вы его (файл) подключаете (include) к разным своим прогам, не меняя. Но при этом исходники нескольких методов этого класса прописываете в mq4-файле, тем самым давая каждой своей проге различный функционал подключаемого класса.

Такое может понадобиться в различных ситуациях. Чаще всего - это какие-нибудь конвертеры/преобразователи. 

 

Представление классов в таком виде воспринимаю как бардак. Это терпимо, если методов немного. Но если список их велик, то при таком способе представления не видно чёткой структуры класса и изучение его затрудняется. 

Хорошим примером считаю стандартную библиотеку MQL. Всё очень аккуратно оформлено. Можно даже сказать - идеально.

 
zaskok:

Пишу, как и Вы, - исходники методов внутри классов. Но бывают ситуации, когда это "неудобно".

Например, у вас определен класс ЧАСТИЧНО (не все методы имеют исходники) в mqh-файле. И вы его (файл) подключаете (include) к разным своим прогам, не меняя. Но при этом исходники нескольких методов этого класса прописываете в mq4-файле, тем самым давая каждой своей проге различный функционал подключаемого класса.

Такое может понадобиться в различных ситуациях. Чаще всего - это какие-нибудь конвертеры/преобразователи. 

Мысль понял, подход рабочий, хотя и неправильный. Например, меняете что-то в .mqh, хотя бы одну переменную в метод добавили и все, надо перелопачивать все .mq4. Вообще, для этого в других языках существуют интерфейсы и абстрактные классы, но в MQL их нет.
А чем вам виртуальные функции не подходят, идеальный же вариант..
 
tol64:

Представление классов в таком виде воспринимаю как бардак. Это терпимо, если методов немного. Но если список их велик, то при таком способе представления не видно чёткой структуры класса и изучение его затрудняется. 

Хорошим примером считаю стандартную библиотеку MQL. Всё очень аккуратно оформлено. Можно даже сказать - идеально.

Я вот на C# давно программирую и бардака не ощущаю. А знаете почему? Потому есть мощная IDE с кучей вариантов представления информации. Конечно, MQ молодцы и проделали отличную работу, но за командой MS им, понятное дело, не угнаться.

В Visual Studio можно нажать collaps to definition и все в выбранной области сжимается до определений.

Хотя, мне в 95% хватает вот такого простейшего средства. Это мой проект на MQL4, который я загнал в студию, потому что иногда не хватает редактора MQL для анализа и, тем более, рефакторинга.


 
VDev:

Я вот на C# давно программирую и бардака не ощущаю. А знаете почему? Потому есть мощная IDE с кучей вариантов представления информации. Конечно, MQ молодцы и проделали отличную работу, но за командой MS им, понятное дело, не угнаться.

В Visual Studio можно нажать collaps to definition и все в выбранной области сжимается до определений.

Хотя, мне в 95% хватает вот такого простейшего средства. Это мой проект на MQL4, который я загнал в студию, потому что иногда не хватает редактора MQL для анализа и, тем более, рефакторинга.

Моё мнение остаётся прежним, так как считаю, что код должен быть таким, чтобы удобство его чтения/изучения не зависело от среды, по средством которой он изучается.
 
tol64:

Представление классов в таком виде воспринимаю как бардак. Это терпимо, если методов немного. Но если список их велик, то при таком способе представления не видно чёткой структуры класса и изучение его затрудняется

А лучше сразу сказать, что просто нет элементарного - фолдинга.
 

VDev:
Мысль понял, подход рабочий, хотя и неправильный. Например, меняете что-то в .mqh, хотя бы одну переменную в метод добавили и все, надо перелопачивать все .mq4. 

Как-то не испытал на себе подобной проблемы.

Вообще, для этого в других языках существуют интерфейсы и абстрактные классы, но в MQL их нет. А чем вам виртуальные функции не подходят, идеальный же вариант..

Знал, что возникнет такой вопрос. Тут ответить возможно только практической реализацией, где отлично видно, что выделенное просто не будет таким удобным, как решение, что описал.
 
Ну, в принципе, мотивация к разделению объявления и реализации методов понятна. А все же хотелось бы иметь в MQL4/5 полноценные проекты. Я это делаю сам, вручную, но это все кустарщина, хочется красиво ))
 
tol64:

Представление классов в таком виде воспринимаю как бардак. Это терпимо, если методов немного. Но если список их велик, то при таком способе представления не видно чёткой структуры класса и изучение его затрудняется. 

Хорошим примером считаю стандартную библиотеку MQL. Всё очень аккуратно оформлено. Можно даже сказать - идеально.


Полностью поддерживаю.

Лично я метод внутри класса описываю только если он содержит одну строку. (Как правило, что-то типа int GetValue() ). В крайнем случае - три-пять строк, не более.

Во всех остальных случаях пишу классы в паре файлов mqh-mq5.

 
VDev:
 

Раздельную компиляцию MQL не поддерживает, нет даже возможности создать проект с несколькими .mq4/5 файлами. Выносить в отдельный заголовочник определения методов класса тоже вроде нельзя (или можно?). В общем было бы интересно услышать мотивацию от представителей MQ.

P.S Кстати, как вы знаете, в современных языках типа C# вообще отказались от заголовочных файлов ввиду ненужности и код пишется в нижеприведенном стиле.

У меня ВСЕ классы имеют исключительно mqh-mq5 структуру. Для примера, моя реализация вашего класса CLogFile:

Файл LOG.MQH:


#include <Files\FileTxt.mqh>
#include <MyLib\Common\GlobalConsts.mqh>

// Дефайн, управляющий расположением глобального лог-файла (если он есть)
// Если нужен отдельный лог-файл для каждого терминала - определить этот дефайн.
// #define GLOBAL_LOG_IS_IN_PRIVATE_FOLDER


#define TIMEDATE_TIMEFORMAT_MODE TIME_DATE | TIME_SECONDS
#define TIME_TIMEFORMAT_MODE TIME_SECONDS
#define DATE_TIMEFORMAT_MODE TIME_DATE


class CLogFile:public CObject
{
public:

   enum ELogMode
      {
      LM_NONE,
      LM_OVERWRITE,
      LM_APPEND
      };

   enum ETimestampMode
      {
      TM_NONE,
      TM_TIMEDATE,
      TM_TIME,
      TM_DATE
      };


protected:

static const string LOG_OPEN_STR;
static const string LOG_CLOSE_STR;
static const string LOG_PATH_STR;


   CFileTxt          m_ftFile;

   ELogMode          m_lmLogMode;
   bool              m_bWriteToLogOpenClose;
   bool              m_bFlushEveryOperation;
   int               m_iTimeToStringMode;
   bool              m_bCommon;

   // Функция просто пишет очередную строку в конец файла c добавлением символа "\n".  
   bool        _LogString(string strLogStr);

   // Функции добавляют строку с сообщением об открытии файла
   bool       _LogOpen();
   bool       _LogClose(); 
  
   // Функция, которая в DEBUG версии выдает указанный комментарий
   // В RELEASE версии ничего не происходит.
   void        _DebugComment(string strMessage);


public:
   CLogFile(ELogMode lmMode=LM_APPEND,ETimestampMode tmTimeMode=TM_TIMEDATE,bool bWriteToLogOpenCloseOfLogFile=false,bool bFlushEveryOperation=false,bool bCommon = true);
   virtual          ~CLogFile();
  
   bool        Open(string sLogFileName);
   void        Close();
   void        Flush();
   bool        IsOpened() { return(m_ftFile.Handle()!=INVALID_HANDLE);};
   string      GetName() { return(m_ftFile.FileName()); };
  

   void        SetTimestampMode(ETimestampMode tsMode);
   bool        LogString(string strLogStr);
   bool        LogString(string strLogStr1,string strLogStr2,string strSeparator = " ");
   bool        LogEmptyString();

   // Функция добавляет строку в глобальный логфайл
   static void GlobalLogString(string strOutput,string strFilename = MAIN_LOG_NAME);
  
   // Функция добавляет строку в логфайл с указанным названием
   static bool AddStringToLog(string strOutput,string strFilename,bool bClearContentsBeforeAdding = false);
};


Файл .MQ5:

#property library

#include <MyLib\Common\GlobalConsts.mqh>
#include <MyLib\Common\Log.mqh>

const string CLogFile::LOG_OPEN_STR = "Log file has opened.";
const string CLogFile::LOG_CLOSE_STR = "Log file has closed.";
const string CLogFile::LOG_PATH_STR = "Log file path: ";

// Gеременная логфайла, в которую складываются все трассировочные сообщения
// Спецификатор static отсутствует - билд 930 перестал позволять использовать в статических переменных нестатические члены класса.
#ifdef GLOBAL_LOG_IS_IN_PRIVATE_FOLDER
CLogFile lfGMainLogFile(LM_OVERWRITE,TM_TIMEDATE,true,true,false);
#else // GLOBAL_LOG_IS_IN_PRIVATE_FOLDER
CLogFile lfGMainLogFile(LM_OVERWRITE,TM_TIMEDATE,true,true,true);
#endif // GLOBAL_LOG_IS_IN_PRIVATE_FOLDER


bool CLogFile::_LogString(string strLogStr)
{
   if(m_lmLogMode == LM_NONE)
      return(true);

   m_ftFile.Seek(NULL,SEEK_END);
  
   if(m_ftFile.IsEnding() !=true)
      return(false);
  
   long lStrLen = StringLen(strLogStr);     
   long lStrWritten = m_ftFile.WriteString(strLogStr+STRING_TERMINATOR);
  
   // В данной проверке важно, что STRING_TERMINATOR_LENGTH у нас 2 символа (CRLF), но в строке эти символы считаются за один "\n"
   if(lStrLen + STRING_TERMINATOR_LENGTH != lStrWritten)
      return(false);

   if(m_bFlushEveryOperation)
      m_ftFile.Flush(); // exception

   return(true);
}


bool CLogFile::_LogOpen()
{
    if(m_bWriteToLogOpenClose == false)   
        return(true);
   
    string strResStr;

   if(m_iTimeToStringMode != NULL)
      strResStr = TimeToString(TimeLocal(),m_iTimeToStringMode) + SPACE_SYMBOL;

    strResStr += LOG_OPEN_STR;
    strResStr += SPACE_SYMBOL;
    strResStr += LOG_PATH_STR;
    strResStr += m_ftFile.FileName();

    return(_LogString(strResStr));
}


bool CLogFile::_LogClose()
{
    if(m_bWriteToLogOpenClose == false)   
        return(true);

   string strResStr;

   if(m_iTimeToStringMode != NULL)
      strResStr = TimeToString(TimeLocal(),m_iTimeToStringMode) + SPACE_SYMBOL;

   strResStr += LOG_CLOSE_STR;
   strResStr += SPACE_SYMBOL;
   strResStr += LOG_PATH_STR;
   strResStr += m_ftFile.FileName();

   return(_LogString(strResStr));
}

void CLogFile::_DebugComment(string strMessage)
{
#ifdef ASSERTION_CODE_ON
      Comment(strMessage);
#endif // ASSERTION_CODE_ON   
};

CLogFile::CLogFile(ELogMode lmMode,ETimestampMode tmTimeMode,bool bWriteToLogOpenCloseOfLogFile,bool bFlushEveryOperation,bool bCommon)
{
   m_lmLogMode=lmMode;
   m_bWriteToLogOpenClose = bWriteToLogOpenCloseOfLogFile;
   m_bFlushEveryOperation = bFlushEveryOperation;
   m_bCommon = bCommon;

   SetTimestampMode(tmTimeMode);
};

CLogFile::~CLogFile()
{
   Close();
};


void CLogFile::Flush()
{
   if(m_ftFile.Handle() !=INVALID_HANDLE)
      m_ftFile.Flush();
}

void CLogFile::Close()
{
   if(m_ftFile.Handle() ==INVALID_HANDLE)
      return;

    _LogClose();

    m_ftFile.Close();
}



void CLogFile::SetTimestampMode(ETimestampMode tmMode)
{
   switch(tmMode)
      {
      case TM_TIMEDATE:
         m_iTimeToStringMode=TIMEDATE_TIMEFORMAT_MODE;
         return;

      case TM_TIME:
         m_iTimeToStringMode=TIME_TIMEFORMAT_MODE;
         return;

      case TM_DATE:
         m_iTimeToStringMode=DATE_TIMEFORMAT_MODE;
         return;

      default:
         //ASSERT(false);

      case TM_NONE:
         m_iTimeToStringMode=NULL;
      }
};

bool CLogFile::Open(string sLogFileName)
{
   //ASSERT(sLogFileName != NULL);
   //ASSERT(INVALID_HANDLE == -1);    // Данная проверка необходима, потому, что в разных местах указывается то одно, то другое значения.

   // Сперва закроем файл, если он открыт.
   if(m_ftFile.Handle()!=INVALID_HANDLE)
      {
      string strOpenedFilename = m_ftFile.FileName();
     
      _LogClose();
     
      m_ftFile.Close();
     
      if(m_ftFile.Handle()!=INVALID_HANDLE)
         {
         _DebugComment("Был открыт файл " + strOpenedFilename + ", но его не удалось закрыть !");
         Print("Был открыт файл " + strOpenedFilename + ", но его не удалось закрыть !");
         return(false);
         }
      };

   int iFlags;

   if(m_bCommon)
      iFlags = FILE_COMMON;
   else    
      iFlags = 0;

   // Удалим файл, если это необходимо.
   if(m_lmLogMode == LM_OVERWRITE && FileIsExist(sLogFileName,iFlags))
      {
      if(FileDelete(sLogFileName,iFlags) != true)
         {
         _DebugComment("Файл " + sLogFileName + " не удалось удалить !");
         Print("Файл " + sLogFileName + " не удалось удалить !");
         return(false);
         };
      }
  

   if(m_bCommon)
      iFlags = FILE_READ|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON;
   else     
      iFlags = FILE_READ|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ;
  

   if(m_ftFile.Open(sLogFileName,iFlags)==INVALID_HANDLE)
      {
      Print("Файл " + sLogFileName + " не удалось открыть !");
      _DebugComment("Файл " + sLogFileName + " не удалось открыть !");
     
      return(false);
      };

    _LogOpen();
   
    return(true);
};

bool CLogFile::LogString(string strLogStr)
{
   string strResStr;

   if(m_iTimeToStringMode != NULL)
      strResStr = TimeToString(TimeLocal(),m_iTimeToStringMode) + SPACE_SYMBOL;

   strResStr += strLogStr;

   bool bRes = _LogString(strResStr);
  
   if(bRes == false)
      {
      _DebugComment("Ошибка CLogFile::LogString() !!! Не удалось записать строоку: " + strLogStr);
      Print("Ошибка CLogFile::LogString() !!! Не удалось записать строоку" + strLogStr);
      }
  
   return(bRes);
};

bool CLogFile::LogEmptyString()
{
   return(LogString(""));
};


bool CLogFile::LogString(string strLogStr1,string strLogStr2,string strSeparator)
{
 string strBuffer; 
 
 StringConcatenate(strLogStr1,strSeparator,strLogStr2);
 
   return(LogString(strBuffer));
};

void CLogFile::GlobalLogString(string strOutput,string strFilename="MainLog.log")
{
   if(lfGMainLogFile.IsOpened() == false || lfGMainLogFile.GetName() != strFilename)
      {
      lfGMainLogFile.Close();
     
      ResetLastError();
     
      bool bRes = lfGMainLogFile.Open(strFilename);
     
      if(bRes == false)
         {
         Print("Необходимо вывести в лог-файл строку: " + strOutput);
         Print("Однако, логфайл " + strFilename + " не удалось открыть.");
         Print("Код ошибки: " + IntegerToString(GetLastError()));
         };
        
      };
  
   lfGMainLogFile.LogString(strOutput);
};

bool CLogFile::AddStringToLog(string strOutput,string strFilename,bool bClearContentsBeforeAdding)
{
   ELogMode lmMode = LM_NONE;

   if(bClearContentsBeforeAdding)
      lmMode = LM_OVERWRITE;
   else   
      lmMode = LM_APPEND;

   CLogFile lfFile(lmMode);
  
   if(lfFile.Open(strFilename) != true)
      return(false);
     
     
   bool bRes = lfFile.LogString(strOutput);
  
   lfFile.Close();

   return(bRes);        
};

Оба файла - можно "сходу" использовать.

В зависимом файле GlobalConsts.h определены дефайны: MAIN_LOG_NAME, STRING_TERMINATOR и SPACE_SYMBOL.

Дефайн  ASSERTION_CODE_ON используется в файлах поддержки DEBUG-RELEASE вариантов.

В сложных классах в .mqh файлах перед каждой public- функцией стоит описание, порой на десятки строк, поясняющее, как ее использовать. На мой взгляд, неразумно "размазывать" эти описания по всему файлу.

Я уж не говорю о том, что, скажем, размер файла .mq5 классa CExpertT (наследний CExpert из Стандартной библиотеки) у меня более 50 килобайт - в таком длинном файле непросто находить нужные функции даже с поиском.

Конечно, если загнать файл в VisualStudio - там, скорее всего, в браузере будет искать проще. Но, как я понимаю, в Visual Studio напрямую комилировать не выйдет, надо постоянно переключаться...